1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 20:07:25 +08:00

Merge branch 'master' into add-bundle-header

This commit is contained in:
Dean Herbert 2020-12-16 13:40:15 +09:00
commit 88b3bf06e8
49 changed files with 1176 additions and 175 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1212.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1214.0" />
</ItemGroup>
</Project>

View File

@ -156,10 +156,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => m is OsuModHidden))
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
// Scale the speed value with accuracy _slightly_
speedValue *= 0.02 + accuracy;
// It is important to also consider accuracy difficulty when doing that
speedValue *= 0.96 + Math.Pow(Attributes.OverallDifficulty, 2) / 1600;
// Scale the speed value with accuracy and OD
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
return speedValue;
}

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
createDrawableRuleset();
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
}
[Test]

View File

@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
IsCentre = (hitObject as Hit)?.Type == HitType.Centre,
IsDrumRoll = hitObject is DrumRoll,
IsSwell = hitObject is Swell,
IsStrong = ((TaikoHitObject)hitObject).IsStrong
IsStrong = (hitObject as TaikoStrongableHitObject)?.IsStrong == true
};
}

View File

@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
{
TaikoHitObject first = x.First();
if (x.Skip(1).Any() && first.CanBeStrong)
first.IsStrong = true;
if (x.Skip(1).Any() && first is TaikoStrongableHitObject strong)
strong.IsStrong = true;
return first;
}).ToList();
}

View File

@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
base.UpdateTernaryStates();
selectionRimState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<TaikoStrongableHitObject>(), h => h.IsStrong);
}
}
}

View File

@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void LoadComplete()
{
base.LoadComplete();
major.BindValueChanged(updateMajor);
major.BindValueChanged(updateMajor, true);
}
private void updateMajor(ValueChangedEvent<bool> major)

View File

@ -19,7 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableDrumRoll : DrawableTaikoHitObject<DrumRoll>
public class DrawableDrumRoll : DrawableTaikoStrongableHitObject<DrumRoll, DrumRoll.StrongNestedHit>
{
/// <summary>
/// Number of rolling hits required to reach the dark/final colour.
@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Content.X = DrawHeight / 2;
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this);
private void updateColour()
{
@ -164,8 +164,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private class StrongNestedHit : DrawableStrongNestedHit
{
public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll)
: base(strong, drumRoll)
public StrongNestedHit(DrumRoll.StrongNestedHit nestedHit, DrawableDrumRoll drumRoll)
: base(nestedHit, drumRoll)
{
}

View File

@ -9,7 +9,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableDrumRollTick : DrawableTaikoHitObject<DrumRollTick>
public class DrawableDrumRollTick : DrawableTaikoStrongableHitObject<DrumRollTick, DrumRollTick.StrongNestedHit>
{
/// <summary>
/// The hit type corresponding to the <see cref="TaikoAction"/> that the user pressed to hit this <see cref="DrawableDrumRollTick"/>.
@ -61,12 +61,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return UpdateResult(true);
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this);
private class StrongNestedHit : DrawableStrongNestedHit
{
public StrongNestedHit(StrongHitObject strong, DrawableDrumRollTick tick)
: base(strong, tick)
public StrongNestedHit(DrumRollTick.StrongNestedHit nestedHit, DrawableDrumRollTick tick)
: base(nestedHit, tick)
{
}

View File

@ -16,7 +16,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableHit : DrawableTaikoHitObject<Hit>
public class DrawableHit : DrawableTaikoStrongableHitObject<Hit, Hit.StrongNestedHit>
{
/// <summary>
/// A list of keys which can result in hits for this HitObject.
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this);
private class StrongNestedHit : DrawableStrongNestedHit
{
@ -240,8 +240,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public new DrawableHit MainObject => (DrawableHit)base.MainObject;
public StrongNestedHit(StrongHitObject strong, DrawableHit hit)
: base(strong, hit)
public StrongNestedHit(Hit.StrongNestedHit nestedHit, DrawableHit hit)
: base(nestedHit, hit)
{
}

View File

@ -7,14 +7,14 @@ using osu.Game.Rulesets.Taiko.Judgements;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
/// <summary>
/// Used as a nested hitobject to provide <see cref="TaikoStrongJudgement"/>s for <see cref="DrawableTaikoHitObject"/>s.
/// Used as a nested hitobject to provide <see cref="TaikoStrongJudgement"/>s for <see cref="DrawableTaikoStrongableHitObject{TObject,TStrongNestedObject}"/>s.
/// </summary>
public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject
{
public readonly DrawableHitObject MainObject;
protected DrawableStrongNestedHit(StrongHitObject strong, DrawableHitObject mainObject)
: base(strong)
protected DrawableStrongNestedHit(StrongNestedHitObject nestedHit, DrawableHitObject mainObject)
: base(nestedHit)
{
MainObject = mainObject;
}

View File

@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
foreach (var t in ticks)
{
if (!t.IsHit)
if (!t.Result.HasResult)
{
nextTick = t;
break;
@ -208,6 +208,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
continue;
}
if (!tick.Result.HasResult)
tick.TriggerResult(false);
}

View File

@ -4,13 +4,11 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Bindings;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
@ -120,112 +118,34 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected Vector2 BaseSize;
protected SkinnableDrawable MainPiece;
private readonly Bindable<bool> isStrong;
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
protected DrawableTaikoHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
isStrong = HitObject.IsStrongBindable.GetBoundCopy();
Anchor = Anchor.CentreLeft;
Origin = Anchor.Custom;
RelativeSizeAxes = Axes.Both;
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
}
[BackgroundDependencyLoader]
private void load()
{
isStrong.BindValueChanged(_ =>
{
// will overwrite samples, should only be called on change.
updateSamplesFromStrong();
RecreatePieces();
});
RecreatePieces();
}
private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
protected override void LoadSamples()
{
base.LoadSamples();
if (HitObject.CanBeStrong)
isStrong.Value = getStrongSamples().Any();
}
private void updateSamplesFromStrong()
{
var strongSamples = getStrongSamples();
if (isStrong.Value != strongSamples.Any())
{
if (isStrong.Value)
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
else
{
foreach (var sample in strongSamples)
HitObject.Samples.Remove(sample);
}
}
}
protected virtual void RecreatePieces()
{
Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
MainPiece?.Expire();
Content.Add(MainPiece = CreateMainPiece());
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
switch (hitObject)
{
case DrawableStrongNestedHit strong:
strongHitContainer.Add(strong);
break;
}
}
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
strongHitContainer.Clear();
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
case StrongHitObject strong:
return CreateStrongHit(strong);
}
return base.CreateNestedHitObject(hitObject);
}
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping).
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
protected abstract SkinnableDrawable CreateMainPiece();
/// <summary>
/// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongHitObject"/>.
/// This is only invoked if <see cref="TaikoHitObject.IsStrong"/> is true for <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The strong hitobject.</param>
/// <returns>The strong hitobject handler.</returns>
protected virtual DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => null;
}
}

View File

@ -0,0 +1,111 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public abstract class DrawableTaikoStrongableHitObject<TObject, TStrongNestedObject> : DrawableTaikoHitObject<TObject>
where TObject : TaikoStrongableHitObject
where TStrongNestedObject : StrongNestedHitObject
{
private readonly Bindable<bool> isStrong;
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
protected DrawableTaikoStrongableHitObject(TObject hitObject)
: base(hitObject)
{
isStrong = HitObject.IsStrongBindable.GetBoundCopy();
AddInternal(strongHitContainer = new Container<DrawableStrongNestedHit>());
}
[BackgroundDependencyLoader]
private void load()
{
isStrong.BindValueChanged(_ =>
{
// will overwrite samples, should only be called on change.
updateSamplesFromStrong();
RecreatePieces();
});
}
private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
protected override void LoadSamples()
{
base.LoadSamples();
isStrong.Value = getStrongSamples().Any();
}
private void updateSamplesFromStrong()
{
var strongSamples = getStrongSamples();
if (isStrong.Value != strongSamples.Any())
{
if (isStrong.Value)
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
else
{
foreach (var sample in strongSamples)
HitObject.Samples.Remove(sample);
}
}
}
protected override void RecreatePieces()
{
base.RecreatePieces();
if (HitObject.IsStrong)
Size = BaseSize = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE);
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
switch (hitObject)
{
case DrawableStrongNestedHit strong:
strongHitContainer.Add(strong);
break;
}
}
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
strongHitContainer.Clear();
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
case TStrongNestedObject strong:
return CreateStrongNestedHit(strong);
}
return base.CreateNestedHitObject(hitObject);
}
/// <summary>
/// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongNestedHitObject"/>.
/// This is only invoked if <see cref="TaikoStrongableHitObject.IsStrong"/> is true for <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The strong hitobject.</param>
/// <returns>The strong hitobject handler.</returns>
protected abstract DrawableStrongNestedHit CreateStrongNestedHit(TStrongNestedObject hitObject);
}
}

View File

@ -15,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class DrumRoll : TaikoHitObject, IHasPath
public class DrumRoll : TaikoStrongableHitObject, IHasPath
{
/// <summary>
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@ -109,6 +109,12 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject
{
}
#region LegacyBeatmapEncoder
double IHasDistance.Distance => Duration * Velocity;

View File

@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Judgements;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class DrumRollTick : TaikoHitObject
public class DrumRollTick : TaikoStrongableHitObject
{
/// <summary>
/// Whether this is the first (initial) tick of the slider.
@ -28,5 +28,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject
{
}
}
}

View File

@ -5,7 +5,7 @@ using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class Hit : TaikoHitObject
public class Hit : TaikoStrongableHitObject
{
public readonly Bindable<HitType> TypeBindable = new Bindable<HitType>();
@ -17,5 +17,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
get => TypeBindable.Value;
set => TypeBindable.Value = value;
}
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject
{
}
}
}

View File

@ -7,7 +7,11 @@ using osu.Game.Rulesets.Taiko.Judgements;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class StrongHitObject : TaikoHitObject
/// <summary>
/// Base type for nested strong hits.
/// Used by <see cref="TaikoStrongableHitObject"/>s to represent their strong bonus scoring portions.
/// </summary>
public abstract class StrongNestedHitObject : TaikoHitObject
{
public override Judgement CreateJudgement() => new TaikoStrongJudgement();

View File

@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
set => Duration = value - StartTime;
}
public override bool CanBeStrong => false;
public double Duration { get; set; }
/// <summary>

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@ -19,47 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary>
public const float DEFAULT_SIZE = 0.45f;
/// <summary>
/// Scale multiplier for a strong drawable taiko hit object.
/// </summary>
public const float STRONG_SCALE = 1.4f;
/// <summary>
/// Default size of a strong drawable taiko hit object.
/// </summary>
public const float DEFAULT_STRONG_SIZE = DEFAULT_SIZE * STRONG_SCALE;
public readonly Bindable<bool> IsStrongBindable = new BindableBool();
/// <summary>
/// Whether this <see cref="TaikoHitObject"/> can be made a "strong" (large) hit.
/// </summary>
public virtual bool CanBeStrong => true;
/// <summary>
/// Whether this HitObject is a "strong" type.
/// Strong hit objects give more points for hitting the hit object with both keys.
/// </summary>
public bool IsStrong
{
get => IsStrongBindable.Value;
set
{
if (value && !CanBeStrong)
throw new InvalidOperationException($"Object of type {GetType()} cannot be strong");
IsStrongBindable.Value = value;
}
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
if (IsStrong)
AddNested(new StrongHitObject { StartTime = this.GetEndTime() });
}
public override Judgement CreateJudgement() => new TaikoJudgement();
protected override HitWindows CreateHitWindows() => new TaikoHitWindows();

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects
{
/// <summary>
/// Base class for taiko hitobjects that can become strong (large).
/// </summary>
public abstract class TaikoStrongableHitObject : TaikoHitObject
{
/// <summary>
/// Scale multiplier for a strong drawable taiko hit object.
/// </summary>
public const float STRONG_SCALE = 1.4f;
/// <summary>
/// Default size of a strong drawable taiko hit object.
/// </summary>
public const float DEFAULT_STRONG_SIZE = DEFAULT_SIZE * STRONG_SCALE;
public readonly Bindable<bool> IsStrongBindable = new BindableBool();
/// <summary>
/// Whether this HitObject is a "strong" type.
/// Strong hit objects give more points for hitting the hit object with both keys.
/// </summary>
public bool IsStrong
{
get => IsStrongBindable.Value;
set => IsStrongBindable.Value = value;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
if (IsStrong)
AddNested(CreateStrongNestedHit(this.GetEndTime()));
}
/// <summary>
/// Creates a <see cref="StrongNestedHitObject"/> representing a second hit on this object.
/// This is only called if <see cref="IsStrong"/> is true.
/// </summary>
/// <param name="startTime">The start time of the nested hit.</param>
protected abstract StrongNestedHitObject CreateStrongNestedHit(double startTime);
}
}

View File

@ -102,13 +102,13 @@ namespace osu.Game.Rulesets.Taiko.Replays
if (hit.Type == HitType.Centre)
{
actions = h.IsStrong
actions = hit.IsStrong
? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre }
: new[] { hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre };
}
else
{
actions = h.IsStrong
actions = hit.IsStrong
? new[] { TaikoAction.LeftRim, TaikoAction.RightRim }
: new[] { hitButton ? TaikoAction.LeftRim : TaikoAction.RightRim };
}

View File

@ -7,7 +7,7 @@ using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
const string normal_hit = "taikohit";
const string big_hit = "taikobig";
string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit;
string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit;
return skin.GetAnimation($"{prefix}{lookup}", true, false) ??
// fallback to regular size if "big" version doesn't exist.

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.UI
/// </summary>
public void VisualiseSecondHit()
{
this.ResizeTo(new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), 50);
this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
}
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(border_thickness, (1 - TaikoHitObject.DEFAULT_STRONG_SIZE) / 2f),
Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE) / 2f),
Alpha = 0.1f
},
new CircularContainer
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
Size = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE),
Masking = true,
BorderColour = Color4.White,
BorderThickness = border_thickness,
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(border_thickness, (1 - TaikoHitObject.DEFAULT_STRONG_SIZE) / 2f),
Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE) / 2f),
Alpha = 0.1f
},
};

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public class TestSceneGameplayLeaderboard : OsuTestScene
{
private readonly TestGameplayLeaderboard leaderboard;
private readonly BindableDouble playerScore = new BindableDouble();
public TestSceneGameplayLeaderboard()
{
Add(leaderboard = new TestGameplayLeaderboard
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2),
RelativeSizeAxes = Axes.X,
});
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("reset leaderboard", () =>
{
leaderboard.Clear();
playerScore.Value = 1222333;
});
AddStep("add player user", () => leaderboard.AddPlayer(playerScore, new User { Username = "You" }));
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
}
[Test]
public void TestPlayerScore()
{
var player2Score = new BindableDouble(1234567);
var player3Score = new BindableDouble(1111111);
AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" }));
AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" }));
AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1));
AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2));
AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500);
AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
AddAssert("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2));
AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3));
AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456);
AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1));
AddAssert("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2));
AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3));
}
private class TestGameplayLeaderboard : GameplayLeaderboard
{
public bool CheckPositionByUsername(string username, int? expectedPosition)
{
var scoreItem = this.FirstOrDefault(i => i.User.Username == username);
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
}
}
}
}

View File

@ -6,6 +6,7 @@ namespace osu.Game.Online.Multiplayer
public enum RoomCategory
{
Normal,
Spotlight
Spotlight,
Realtime,
}
}

View File

@ -0,0 +1,65 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
namespace osu.Game.Online.RealtimeMultiplayer
{
/// <summary>
/// An interface defining a multiplayer client instance.
/// </summary>
public interface IMultiplayerClient
{
/// <summary>
/// Signals that the room has changed state.
/// </summary>
/// <param name="state">The state of the room.</param>
Task RoomStateChanged(MultiplayerRoomState state);
/// <summary>
/// Signals that a user has joined the room.
/// </summary>
/// <param name="user">The user.</param>
Task UserJoined(MultiplayerRoomUser user);
/// <summary>
/// Signals that a user has left the room.
/// </summary>
/// <param name="user">The user.</param>
Task UserLeft(MultiplayerRoomUser user);
/// <summary>
/// Signal that the host of the room has changed.
/// </summary>
/// <param name="userId">The user ID of the new host.</param>
Task HostChanged(long userId);
/// <summary>
/// Signals that the settings for this room have changed.
/// </summary>
/// <param name="newSettings">The updated room settings.</param>
Task SettingsChanged(MultiplayerRoomSettings newSettings);
/// <summary>
/// Signals that a user in this room changed their state.
/// </summary>
/// <param name="userId">The ID of the user performing a state change.</param>
/// <param name="state">The new state of the user.</param>
Task UserStateChanged(long userId, MultiplayerUserState state);
/// <summary>
/// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point.
/// </summary>
Task LoadRequested();
/// <summary>
/// Signals that a match has started. All users in the <see cref="MultiplayerUserState.Loaded"/> state should begin gameplay as soon as possible.
/// </summary>
Task MatchStarted();
/// <summary>
/// Signals that the match has ended, all players have finished and results are ready to be displayed.
/// </summary>
Task ResultsReady();
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
namespace osu.Game.Online.RealtimeMultiplayer
{
/// <summary>
/// Interface for an out-of-room multiplayer server.
/// </summary>
public interface IMultiplayerLoungeServer
{
/// <summary>
/// Request to join a multiplayer room.
/// </summary>
/// <param name="roomId">The databased room ID.</param>
/// <exception cref="InvalidStateException">If the user is already in the requested (or another) room.</exception>
Task<MultiplayerRoom> JoinRoom(long roomId);
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
namespace osu.Game.Online.RealtimeMultiplayer
{
/// <summary>
/// Interface for an in-room multiplayer server.
/// </summary>
public interface IMultiplayerRoomServer
{
/// <summary>
/// Request to leave the currently joined room.
/// </summary>
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
Task LeaveRoom();
/// <summary>
/// Transfer the host of the currently joined room to another user in the room.
/// </summary>
/// <param name="userId">The new user which is to become host.</param>
/// <exception cref="NotHostException">A user other than the current host is attempting to transfer host.</exception>
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
Task TransferHost(long userId);
/// <summary>
/// As the host, update the settings of the currently joined room.
/// </summary>
/// <param name="settings">The new settings to apply.</param>
/// <exception cref="NotHostException">A user other than the current host is attempting to transfer host.</exception>
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
Task ChangeSettings(MultiplayerRoomSettings settings);
/// <summary>
/// Change the local user state in the currently joined room.
/// </summary>
/// <param name="newState">The proposed new state.</param>
/// <exception cref="InvalidStateChangeException">If the state change requested is not valid, given the previous state or room state.</exception>
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
Task ChangeState(MultiplayerUserState newState);
/// <summary>
/// As the host of a room, start the match.
/// </summary>
/// <exception cref="NotHostException">A user other than the current host is attempting to start the game.</exception>
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
/// <exception cref="InvalidStateException">If an attempt to start the game occurs when the game's (or users') state disallows it.</exception>
Task StartMatch();
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.RealtimeMultiplayer
{
/// <summary>
/// An interface defining the multiplayer server instance.
/// </summary>
public interface IMultiplayerServer : IMultiplayerRoomServer, IMultiplayerLoungeServer
{
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.RealtimeMultiplayer
{
[Serializable]
public class InvalidStateChangeException : HubException
{
public InvalidStateChangeException(MultiplayerUserState oldState, MultiplayerUserState newState)
: base($"Cannot change from {oldState} to {newState}")
{
}
protected InvalidStateChangeException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.RealtimeMultiplayer
{
[Serializable]
public class InvalidStateException : HubException
{
public InvalidStateException(string message)
: base(message)
{
}
protected InvalidStateException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -0,0 +1,76 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using Newtonsoft.Json;
using osu.Framework.Allocation;
namespace osu.Game.Online.RealtimeMultiplayer
{
/// <summary>
/// A multiplayer room.
/// </summary>
[Serializable]
public class MultiplayerRoom
{
/// <summary>
/// The ID of the room, used for database persistence.
/// </summary>
public readonly long RoomID;
/// <summary>
/// The current state of the room (ie. whether it is in progress or otherwise).
/// </summary>
public MultiplayerRoomState State { get; set; }
/// <summary>
/// All currently enforced game settings for this room.
/// </summary>
public MultiplayerRoomSettings Settings { get; set; } = new MultiplayerRoomSettings();
/// <summary>
/// All users currently in this room.
/// </summary>
public List<MultiplayerRoomUser> Users { get; set; } = new List<MultiplayerRoomUser>();
/// <summary>
/// The host of this room, in control of changing room settings.
/// </summary>
public MultiplayerRoomUser? Host { get; set; }
private object writeLock = new object();
[JsonConstructor]
public MultiplayerRoom(in long roomId)
{
RoomID = roomId;
}
private object updateLock = new object();
private ManualResetEventSlim freeForWrite = new ManualResetEventSlim(true);
/// <summary>
/// Request a lock on this room to perform a thread-safe update.
/// </summary>
public IDisposable LockForUpdate()
{
// ReSharper disable once InconsistentlySynchronizedField
freeForWrite.Wait();
lock (updateLock)
{
freeForWrite.Wait();
freeForWrite.Reset();
return new ValueInvokeOnDisposal<MultiplayerRoom>(this, r => freeForWrite.Set());
}
}
public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]";
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Online.API;
namespace osu.Game.Online.RealtimeMultiplayer
{
[Serializable]
public class MultiplayerRoomSettings : IEquatable<MultiplayerRoomSettings>
{
public int BeatmapID { get; set; }
public int RulesetID { get; set; }
public string Name { get; set; } = "Unnamed room";
[NotNull]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && Name.Equals(other.Name, StringComparison.Ordinal);
public override string ToString() => $"Name:{Name} Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}";
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
namespace osu.Game.Online.RealtimeMultiplayer
{
/// <summary>
/// The current overall state of a realtime multiplayer room.
/// </summary>
public enum MultiplayerRoomState
{
/// <summary>
/// The room is open and accepting new players.
/// </summary>
Open,
/// <summary>
/// A game start has been triggered but players have not finished loading.
/// </summary>
WaitingForLoad,
/// <summary>
/// A game is currently ongoing.
/// </summary>
Playing,
/// <summary>
/// The room has been disbanded and closed.
/// </summary>
Closed
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using Newtonsoft.Json;
using osu.Game.Users;
namespace osu.Game.Online.RealtimeMultiplayer
{
[Serializable]
public class MultiplayerRoomUser : IEquatable<MultiplayerRoomUser>
{
public readonly int UserID;
public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle;
public User? User { get; set; }
[JsonConstructor]
public MultiplayerRoomUser(in int userId)
{
UserID = userId;
}
public bool Equals(MultiplayerRoomUser other)
{
if (ReferenceEquals(this, other)) return true;
return UserID == other.UserID;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((MultiplayerRoomUser)obj);
}
public override int GetHashCode() => UserID.GetHashCode();
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.RealtimeMultiplayer
{
public enum MultiplayerUserState
{
/// <summary>
/// The user is idle and waiting for something to happen (or watching the match but not participating).
/// </summary>
Idle,
/// <summary>
/// The user has marked themselves as ready to participate and should be considered for the next game start.
/// </summary>
/// <remarks>
/// Clients in this state will receive gameplay channel messages.
/// As a client the only thing to look for in this state is a <see cref="IMultiplayerClient.LoadRequested"/> call.
/// </remarks>
Ready,
/// <summary>
/// The server is waiting for this user to finish loading. This is a reserved state, and is set by the server.
/// </summary>
/// <remarks>
/// All users in <see cref="Ready"/> state when the game start will be transitioned to this state.
/// All users in this state need to transition to <see cref="Loaded"/> before the game can start.
/// </remarks>
WaitingForLoad,
/// <summary>
/// The user's client has marked itself as loaded and ready to begin gameplay.
/// </summary>
Loaded,
/// <summary>
/// The user is currently playing in a game. This is a reserved state, and is set by the server.
/// </summary>
/// <remarks>
/// Once there are no remaining <see cref="WaitingForLoad"/> users, all users in <see cref="Loaded"/> state will be transitioned to this state.
/// At this point the game will start for all users.
/// </remarks>
Playing,
/// <summary>
/// The user has finished playing and is ready to view results.
/// </summary>
/// <remarks>
/// Once all users transition from <see cref="Playing"/> to this state, the game will end and results will be distributed.
/// All users will be transitioned to the <see cref="Results"/> state.
/// </remarks>
FinishedPlay,
/// <summary>
/// The user is currently viewing results. This is a reserved state, and is set by the server.
/// </summary>
Results,
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.RealtimeMultiplayer
{
[Serializable]
public class NotHostException : HubException
{
public NotHostException()
: base("User is attempting to perform a host level operation while not the host")
{
}
protected NotHostException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.RealtimeMultiplayer
{
[Serializable]
public class NotJoinedRoomException : HubException
{
public NotJoinedRoomException()
: base("This user has not yet joined a multiplayer room.")
{
}
protected NotJoinedRoomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

View File

@ -152,7 +152,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
}
else
{
foreach (var b in SetInfo.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty))
foreach (var b in SetInfo.Beatmaps.OrderBy(beatmap => beatmap.Ruleset.ID).ThenBy(beatmap => beatmap.StarDifficulty))
icons.Add(new DifficultyIcon(b));
}

View File

@ -4,6 +4,8 @@
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Game.Screens.Import;
namespace osu.Game.Overlays.Settings.Sections.Debug
{
@ -11,8 +13,8 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
{
protected override string Header => "General";
[BackgroundDependencyLoader]
private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig)
[BackgroundDependencyLoader(true)]
private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, OsuGame game)
{
Children = new Drawable[]
{
@ -27,6 +29,11 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
Current = config.GetBindable<bool>(DebugSetting.BypassFrontToBackPass)
}
};
Add(new SettingsButton
{
Text = "Import files",
Action = () => game?.PerformFromScreen(menu => menu.Push(new FileImportScreen()))
});
}
}
}

View File

@ -0,0 +1,168 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
namespace osu.Game.Screens.Import
{
public class FileImportScreen : OsuScreen
{
public override bool HideOverlaysOnEnter => true;
private FileSelector fileSelector;
private Container contentContainer;
private TextFlowContainer currentFileText;
private TriangleButton importButton;
private const float duration = 300;
private const float button_height = 50;
private const float button_vertical_margin = 15;
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader(true)]
private void load(Storage storage)
{
InternalChild = contentContainer = new Container
{
Masking = true,
CornerRadius = 10,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.9f, 0.8f),
Children = new Drawable[]
{
new Box
{
Colour = colours.GreySeafoamDark,
RelativeSizeAxes = Axes.Both,
},
fileSelector = new FileSelector(validFileExtensions: game.HandledExtensions.ToArray())
{
RelativeSizeAxes = Axes.Both,
Width = 0.65f
},
new Container
{
RelativeSizeAxes = Axes.Both,
Width = 0.35f,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Children = new Drawable[]
{
new Box
{
Colour = colours.GreySeafoamDarker,
RelativeSizeAxes = Axes.Both
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = button_height + button_vertical_margin * 2 },
Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Child = currentFileText = new TextFlowContainer(t => t.Font = OsuFont.Default.With(size: 30))
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
TextAnchor = Anchor.Centre
},
ScrollContent =
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
},
},
importButton = new TriangleButton
{
Text = "Import",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = button_height,
Width = 0.9f,
Margin = new MarginPadding { Vertical = button_vertical_margin },
Action = () => startImport(fileSelector.CurrentFile.Value?.FullName)
}
}
}
}
};
fileSelector.CurrentFile.BindValueChanged(fileChanged, true);
fileSelector.CurrentPath.BindValueChanged(directoryChanged);
}
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
contentContainer.ScaleTo(0.95f).ScaleTo(1, duration, Easing.OutQuint);
this.FadeInFromZero(duration);
}
public override bool OnExiting(IScreen next)
{
contentContainer.ScaleTo(0.95f, duration, Easing.OutQuint);
this.FadeOut(duration, Easing.OutQuint);
return base.OnExiting(next);
}
private void directoryChanged(ValueChangedEvent<DirectoryInfo> _)
{
// this should probably be done by the selector itself, but let's do it here for now.
fileSelector.CurrentFile.Value = null;
}
private void fileChanged(ValueChangedEvent<FileInfo> selectedFile)
{
importButton.Enabled.Value = selectedFile.NewValue != null;
currentFileText.Text = selectedFile.NewValue?.Name ?? "Select a file";
}
private void startImport(string path)
{
if (string.IsNullOrEmpty(path))
return;
Task.Factory.StartNew(async () =>
{
await game.Import(path);
// some files will be deleted after successful import, so we want to refresh the view.
Schedule(() =>
{
// should probably be exposed as a refresh method.
fileSelector.CurrentPath.TriggerChange();
});
}, TaskCreationOptions.LongRunning);
}
}
}

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class GameplayLeaderboard : FillFlowContainer<GameplayLeaderboardScore>
{
public GameplayLeaderboard()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Spacing = new Vector2(2.5f);
LayoutDuration = 250;
LayoutEasing = Easing.OutQuint;
}
/// <summary>
/// Adds a player to the leaderboard.
/// </summary>
/// <param name="currentScore">The bindable current score of the player.</param>
/// <param name="user">The player.</param>
public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user)
{
var scoreItem = addScore(currentScore.Value, user);
currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue;
}
private GameplayLeaderboardScore addScore(double totalScore, User user)
{
var scoreItem = new GameplayLeaderboardScore
{
User = user,
TotalScore = totalScore,
OnScoreChange = updateScores,
};
Add(scoreItem);
updateScores();
return scoreItem;
}
private void updateScores()
{
var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList();
for (int i = 0; i < Count; i++)
{
SetLayoutPosition(orderedByScore[i], i);
orderedByScore[i].ScorePosition = i + 1;
}
}
}
}

View File

@ -0,0 +1,136 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class GameplayLeaderboardScore : CompositeDrawable
{
private readonly OsuSpriteText positionText, positionSymbol, userString;
private readonly GlowingSpriteText scoreText;
public Action OnScoreChange;
private int? scorePosition;
public int? ScorePosition
{
get => scorePosition;
set
{
scorePosition = value;
if (scorePosition.HasValue)
positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}";
positionText.FadeTo(scorePosition.HasValue ? 1 : 0);
positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0);
}
}
private double totalScore;
public double TotalScore
{
get => totalScore;
set
{
totalScore = value;
scoreText.Text = totalScore.ToString("N0");
OnScoreChange?.Invoke();
}
}
private User user;
public User User
{
get => user;
set
{
user = value;
userString.Text = user?.Username;
}
}
public GameplayLeaderboardScore()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new Container
{
Masking = true,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Right = 2.5f },
Spacing = new Vector2(2.5f),
Children = new[]
{
positionText = new OsuSpriteText
{
Alpha = 0,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
},
positionSymbol = new OsuSpriteText
{
Alpha = 0,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
Text = ">",
},
}
},
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Left = 2.5f },
Spacing = new Vector2(2.5f),
Children = new Drawable[]
{
userString = new OsuSpriteText
{
Size = new Vector2(80, 16),
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
},
scoreText = new GlowingSpriteText
{
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
Font = OsuFont.Numeric.With(size: 14),
}
}
},
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
positionText.Colour = colours.YellowLight;
positionSymbol.Colour = colours.Yellow;
}
}
}

View File

@ -189,7 +189,7 @@ namespace osu.Game.Screens.Play
// after an initial delay, start the debounced load check.
// this will continue to execute even after resuming back on restart.
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, 1800, 0));
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0));
showMuteWarningIfNeeded();
}

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1212.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1214.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="Sentry" Version="2.1.8" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1212.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1214.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -88,7 +88,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1212.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1214.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -928,5 +928,6 @@ private void load()
<s:Boolean x:Key="/Default/UserDictionary/Words/=ruleset/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=rulesets/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ruleset_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Strongable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Taiko/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unranked/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>