diff --git a/osu.Android.props b/osu.Android.props index 196d122a2a..c78dfb6a55 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index d06c4b6746..5fb09c0cef 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -69,7 +69,6 @@ namespace osu.Desktop /// Allow a maximum of one unhandled exception, per second of execution. /// /// - /// private static bool handleException(Exception arg) { bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index c81710ed18..26e5d381e2 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -482,7 +482,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// Retrieves the sample info list at a point in time. /// /// The time to retrieve the sample info list from. - /// private IList sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples; /// diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs index 856b6554b9..0ba775e5c7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private void runSpmTest(Mod mod) { - SpinnerSpmCounter spmCounter = null; + SpinnerSpmCalculator spmCalculator = null; CreateModTest(new ModTestData { @@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1 }); - AddUntilStep("fetch SPM counter", () => + AddUntilStep("fetch SPM calculator", () => { - spmCounter = this.ChildrenOfType().SingleOrDefault(); - return spmCounter != null; + spmCalculator = this.ChildrenOfType().SingleOrDefault(); + return spmCalculator != null; }); - AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5)); + AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5)); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 7df5ca0f7c..24e69703a6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Beatmap = singleSpinnerBeatmap, PassCondition = () => { - var counter = Player.ChildrenOfType().SingleOrDefault(); - return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1); + var counter = Player.ChildrenOfType().SingleOrDefault(); + return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1); } }); } diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png new file mode 100644 index 0000000000..73753554f7 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index ac8d5c81bc..14c709cae1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -168,13 +168,13 @@ namespace osu.Game.Rulesets.Osu.Tests double estimatedSpm = 0; addSeekStep(1000); - AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); + AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value); addSeekStep(2000); - AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); + AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); addSeekStep(1000); - AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); + AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); } [TestCase(0.5)] @@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("retrieve spinner state", () => { expectedProgress = drawableSpinner.Progress; - expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute; + expectedSpm = drawableSpinner.SpinsPerMinute.Value; }); addSeekStep(0); @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(1000); AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); - AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0)); + AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); } private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 39e78a14aa..3a4753761a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result; public SpinnerRotationTracker RotationTracker { get; private set; } - public SpinnerSpmCounter SpmCounter { get; private set; } + + private SpinnerSpmCalculator spmCalculator; private Container ticks; private PausableSkinnableSound spinningSample; @@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// public IBindable GainedBonus => gainedBonus; - private readonly Bindable gainedBonus = new Bindable(); + private readonly Bindable gainedBonus = new BindableDouble(); + + /// + /// The number of spins per minute this spinner is spinning at, for display purposes. + /// + public readonly IBindable SpinsPerMinute = new BindableDouble(); private const double fade_out_duration = 160; @@ -63,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + AddRangeInternal(new Drawable[] { + spmCalculator = new SpinnerSpmCalculator + { + Result = { BindTarget = SpinsPerMinute }, + }, ticks = new Container(), new AspectContainer { @@ -77,20 +87,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RotationTracker = new SpinnerRotationTracker(this) } }, - SpmCounter = new SpinnerSpmCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Y = 120, - Alpha = 0 - }, spinningSample = new PausableSkinnableSound { Volume = { Value = 0 }, Looping = true, Frequency = { Value = spinning_sample_initial_frequency } } - }; + }); PositionBindable.BindValueChanged(pos => Position = pos.NewValue); } @@ -161,17 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override void UpdateStartTimeStateTransforms() - { - base.UpdateStartTimeStateTransforms(); - - if (Result?.TimeStarted is double startTime) - { - using (BeginAbsoluteSequence(startTime)) - fadeInCounter(); - } - } - protected override void UpdateHitStateTransforms(ArmedState state) { base.UpdateHitStateTransforms(state); @@ -282,22 +274,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateAfterChildren(); - if (!SpmCounter.IsPresent && RotationTracker.Tracking) - { - Result.TimeStarted ??= Time.Current; - fadeInCounter(); - } + if (Result.TimeStarted == null && RotationTracker.Tracking) + Result.TimeStarted = Time.Current; // don't update after end time to avoid the rate display dropping during fade out. // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period. if (Time.Current <= HitObject.EndTime) - SpmCounter.SetRotation(Result.RateAdjustedRotation); + spmCalculator.SetRotation(Result.RateAdjustedRotation); updateBonusScore(); } - private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn); - private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; private int wholeSpins; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs index 891821fe2f..ae8c03dad1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private OsuSpriteText bonusCounter; + private Container spmContainer; + private OsuSpriteText spmCounter; + public DefaultSpinner() { RelativeSizeAxes = Axes.Both; @@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Origin = Anchor.Centre, Font = OsuFont.Numeric.With(size: 24), Y = -120, + }, + spmContainer = new Container + { + Alpha = 0f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = 120, + Children = new[] + { + spmCounter = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = @"0", + Font = OsuFont.Numeric.With(size: 24) + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = @"SPINS PER MINUTE", + Font = OsuFont.Numeric.With(size: 12), + Y = 30 + } + } } }); } private IBindable gainedBonus; + private IBindable spinsPerMinute; protected override void LoadComplete() { @@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default bonusCounter.FadeOutFromOne(1500); bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); }); + + spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy(); + spinsPerMinute.BindValueChanged(spm => + { + spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0"); + }, true); + + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); + } + + protected override void Update() + { + base.Update(); + + if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null) + fadeCounterOnTimeStart(); + } + + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + { + if (!(drawableHitObject is DrawableSpinner)) + return; + + fadeCounterOnTimeStart(); + } + + private void fadeCounterOnTimeStart() + { + if (drawableSpinner.Result?.TimeStarted is double startTime) + { + using (BeginAbsoluteSequence(startTime)) + spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn); + } } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs similarity index 61% rename from osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs index 69355f624b..a5205bbb8c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs @@ -1,77 +1,37 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; 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.Utils; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Skinning.Default { - public class SpinnerSpmCounter : Container + public class SpinnerSpmCalculator : Component { + private readonly Queue records = new Queue(); + private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues + + /// + /// The resultant spins per minute value, which is updated via . + /// + public IBindable Result => result; + + private readonly Bindable result = new BindableDouble(); + [Resolved] private DrawableHitObject drawableSpinner { get; set; } - private readonly OsuSpriteText spmText; - - public SpinnerSpmCounter() - { - Children = new Drawable[] - { - spmText = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = @"0", - Font = OsuFont.Numeric.With(size: 24) - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = @"SPINS PER MINUTE", - Font = OsuFont.Numeric.With(size: 12), - Y = 30 - } - }; - } - protected override void LoadComplete() { base.LoadComplete(); drawableSpinner.HitObjectApplied += resetState; } - private double spm; - - public double SpinsPerMinute - { - get => spm; - private set - { - if (value == spm) return; - - spm = value; - spmText.Text = Math.Truncate(value).ToString(@"#0"); - } - } - - private struct RotationRecord - { - public float Rotation; - public double Time; - } - - private readonly Queue records = new Queue(); - private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues - public void SetRotation(float currentRotation) { // Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result. @@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration) record = records.Dequeue(); - SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360; + result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360; } records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current }); @@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private void resetState(DrawableHitObject hitObject) { - SpinsPerMinute = 0; + result.Value = 0; records.Clear(); } @@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (drawableSpinner != null) drawableSpinner.HitObjectApplied -= resetState; } + + private struct RotationRecord + { + public float Rotation; + public double Time; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 064b7a4680..7eb6898abc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected const float SPRITE_SCALE = 0.625f; + private const float spm_hide_offset = 50f; + protected DrawableSpinner DrawableSpinner { get; private set; } private Sprite spin; @@ -35,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private LegacySpriteText bonusCounter; + private Sprite spmBackground; + private LegacySpriteText spmCounter; + [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject, ISkinSource source) { @@ -79,11 +84,27 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Scale = new Vector2(SPRITE_SCALE), Y = SPINNER_TOP_OFFSET + 299, }.With(s => s.Font = s.Font.With(fixedWidth: false)), + spmBackground = new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft, + Texture = source.GetTexture("spinner-rpm"), + Scale = new Vector2(SPRITE_SCALE), + Position = new Vector2(-87, 445 + spm_hide_offset), + }, + spmCounter = new LegacySpriteText(source, LegacyFont.Score) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight, + Scale = new Vector2(SPRITE_SCALE * 0.9f), + Position = new Vector2(80, 448 + spm_hide_offset), + }.With(s => s.Font = s.Font.With(fixedWidth: false)), } }); } private IBindable gainedBonus; + private IBindable spinsPerMinute; private readonly Bindable completed = new Bindable(); @@ -99,6 +120,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out); }); + spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy(); + spinsPerMinute.BindValueChanged(spm => + { + spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0"); + }, true); + completed.BindValueChanged(onCompletedChanged, true); DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms; @@ -142,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy switch (drawableHitObject) { case DrawableSpinner d: - double fadeOutLength = Math.Min(400, d.HitObject.Duration); + using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn)) + { + spmBackground.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); + spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); + } - using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true)) - spin.FadeOutFromOne(fadeOutLength); + double spinFadeOutLength = Math.Min(400, d.HitObject.Duration); + + using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true)) + spin.FadeOutFromOne(spinFadeOutLength); break; case DrawableSpinnerTick d: diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 77f910c144..3afb7481b1 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -11,7 +11,10 @@ using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; namespace osu.Game.Tests.Online { @@ -84,6 +87,36 @@ namespace osu.Game.Tests.Online Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11)); } + [Test] + public void TestDeserialiseScoreInfoWithEmptyMods() + { + var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo }; + + var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score)); + + if (deserialised != null) + deserialised.Ruleset = new OsuRuleset().RulesetInfo; + + Assert.That(deserialised?.Mods.Length, Is.Zero); + } + + [Test] + public void TestDeserialiseScoreInfoWithCustomModSetting() + { + var score = new ScoreInfo + { + Ruleset = new OsuRuleset().RulesetInfo, + Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } } + }; + + var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score)); + + if (deserialised != null) + deserialised.Ruleset = new OsuRuleset().RulesetInfo; + + Assert.That(((OsuModDoubleTime)deserialised?.Mods[0])?.SpeedChange.Value, Is.EqualTo(2)); + } + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => new Mod[] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 1ee848b902..b6c06bb149 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Online; using osu.Game.Online.API; @@ -38,6 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + private OsuConfigManager config; + public TestSceneMultiplayerGameplayLeaderboard() { base.Content.Children = new Drawable[] @@ -48,6 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer }; } + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); + } + [SetUpSteps] public override void SetUpSteps() { @@ -97,6 +106,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users); } + [Test] + public void TestChangeScoringMode() + { + AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 5); + AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic)); + AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); + } + public class TestMultiplayerStreaming : SpectatorStreamingClient { public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; @@ -163,7 +180,7 @@ namespace osu.Game.Tests.Visual.Multiplayer break; } - ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty())); + ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) })); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 839118de2f..caa731f985 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); + AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs new file mode 100644 index 0000000000..3b2cfb1c7b --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -0,0 +1,229 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Database; +using osu.Game.Online; +using osu.Game.Online.Spectator; +using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osu.Game.Screens.Play.HUD; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerSpectatorLeaderboard : MultiplayerTestScene + { + [Cached(typeof(SpectatorStreamingClient))] + private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient(); + + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + + protected override Container Content => content; + private readonly Container content; + + private readonly Dictionary clocks = new Dictionary + { + { 55, new ManualClock() }, + { 56, new ManualClock() } + }; + + public TestSceneMultiplayerSpectatorLeaderboard() + { + base.Content.AddRange(new Drawable[] + { + streamingClient, + lookupCache, + content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + + [SetUpSteps] + public new void SetUpSteps() + { + MultiplayerSpectatorLeaderboard leaderboard = null; + + AddStep("reset", () => + { + Clear(); + + foreach (var (userId, clock) in clocks) + { + streamingClient.EndPlay(userId, 0); + clock.CurrentTime = 0; + } + }); + + AddStep("create leaderboard", () => + { + foreach (var (userId, _) in clocks) + streamingClient.StartPlay(userId, 0); + + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + var scoreProcessor = new OsuScoreProcessor(); + scoreProcessor.ApplyBeatmap(playable); + + LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add); + }); + + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + + AddStep("add clock sources", () => + { + foreach (var (userId, clock) in clocks) + leaderboard.AddClock(userId, clock); + }); + } + + [Test] + public void TestLeaderboardTracksCurrentTime() + { + AddStep("send frames", () => + { + // For user 55, send frames in sets of 1. + // For user 56, send frames in sets of 10. + for (int i = 0; i < 100; i++) + { + streamingClient.SendFrames(55, i, 1); + + if (i % 10 == 0) + streamingClient.SendFrames(56, i, 10); + } + }); + + assertCombo(55, 1); + assertCombo(56, 10); + + // Advance to a point where only user 55's frame changes. + setTime(500); + assertCombo(55, 5); + assertCombo(56, 10); + + // Advance to a point where both user's frame changes. + setTime(1100); + assertCombo(55, 11); + assertCombo(56, 20); + + // Advance user 56 only to a point where its frame changes. + setTime(56, 2100); + assertCombo(55, 11); + assertCombo(56, 30); + + // Advance both users beyond their last frame + setTime(101 * 100); + assertCombo(55, 100); + assertCombo(56, 100); + } + + [Test] + public void TestNoFrames() + { + assertCombo(55, 0); + assertCombo(56, 0); + } + + private void setTime(double time) => AddStep($"set time {time}", () => + { + foreach (var (_, clock) in clocks) + clock.CurrentTime = time; + }); + + private void setTime(int userId, double time) + => AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time); + + private void assertCombo(int userId, int expectedCombo) + => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo); + + private class TestSpectatorStreamingClient : SpectatorStreamingClient + { + private readonly Dictionary userBeatmapDictionary = new Dictionary(); + private readonly Dictionary userSentStateDictionary = new Dictionary(); + + public TestSpectatorStreamingClient() + : base(new DevelopmentEndpointConfiguration()) + { + } + + public void StartPlay(int userId, int beatmapId) + { + userBeatmapDictionary[userId] = beatmapId; + userSentStateDictionary[userId] = false; + sendState(userId, beatmapId); + } + + public void EndPlay(int userId, int beatmapId) + { + ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + userSentStateDictionary[userId] = false; + } + + public void SendFrames(int userId, int index, int count) + { + var frames = new List(); + + for (int i = index; i < index + count; i++) + { + var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; + frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); + } + + var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames); + ((ISpectatorClient)this).UserSentFrames(userId, bundle); + if (!userSentStateDictionary[userId]) + sendState(userId, userBeatmapDictionary[userId]); + } + + public override void WatchUser(int userId) + { + if (userSentStateDictionary[userId]) + { + // usually the server would do this. + sendState(userId, userBeatmapDictionary[userId]); + } + + base.WatchUser(userId); + } + + private void sendState(int userId, int beatmapId) + { + ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + userSentStateDictionary[userId] = true; + } + } + + private class TestUserLookupCache : UserLookupCache + { + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) + { + return Task.FromResult(new User + { + Id = lookup, + Username = $"User {lookup}" + }); + } + } + } +} diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 2ee52c35aa..92eb7ac713 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -141,7 +141,6 @@ namespace osu.Game.Tournament /// /// Add missing player info based on user IDs. /// - /// private bool addPlayers() { bool addedInfo = false; diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 9847ea020a..769b33009a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -44,7 +44,6 @@ namespace osu.Game.Beatmaps /// /// Returns statistics for the contained in this beatmap. /// - /// IEnumerable GetStatistics(); /// diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index f8c9bdeaf8..86e84b0732 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.cs @@ -22,7 +22,6 @@ namespace osu.Game.Configuration /// /// The ruleset's internal ID. /// An optional variant. - /// public List Query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 0e9382279a..67cee883c8 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -346,7 +346,6 @@ namespace osu.Game.Graphics.Backgrounds /// such that the smaller triangles appear on top. /// /// - /// public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale); } } diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs index ba188963ea..c8d5ce39a6 100644 --- a/osu.Game/IO/Serialization/IJsonSerializable.cs +++ b/osu.Game/IO/Serialization/IJsonSerializable.cs @@ -22,7 +22,6 @@ namespace osu.Game.IO.Serialization /// /// Creates the default that should be used for all s. /// - /// public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index b25b00eb84..9d0cfedc03 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -85,7 +85,6 @@ namespace osu.Game.Input /// /// The ruleset's internal ID. /// An optional variant. - /// public List Query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 6da9f12b50..d95b246c96 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -574,7 +574,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Calculate the position to be used for sample playback at a specified X position (0..1). /// /// The lookup X position. Generally should be . - /// protected double CalculateSamplePlaybackBalance(double position) { const float balance_adjust_amount = 0.4f; diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 61f5f94142..e64298f98d 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -147,7 +147,6 @@ namespace osu.Game.Rulesets.Objects /// to 1 (end of the path). /// /// Ranges from 0 (beginning of the path) to 1 (end of the path). - /// public Vector2 PositionAt(double progress) { ensureValid(); @@ -161,7 +160,6 @@ namespace osu.Game.Rulesets.Objects /// The first point has a PathType which all other points inherit. /// /// One of the control points in the segment. - /// public List PointsInSegment(PathControlPoint controlPoint) { bool found = false; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 38d30a2e31..efc8b50e3c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -146,7 +146,6 @@ namespace osu.Game.Rulesets /// The beatmap to create the hit renderer for. /// The s to apply. /// Unable to successfully load the beatmap to be usable with this ruleset. - /// public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null); /// diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 018b50bd3d..410614de07 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -62,7 +62,6 @@ namespace osu.Game.Rulesets.Scoring /// /// Retrieves a mapping of s to their timing windows for all allowed s. /// - /// public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index ef11c19e3f..222f69b025 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json.Converters; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -55,9 +56,10 @@ namespace osu.Game.Scoring [JsonIgnore] public virtual RulesetInfo Ruleset { get; set; } + private APIMod[] localAPIMods; private Mod[] mods; - [JsonProperty("mods")] + [JsonIgnore] [NotMapped] public Mod[] Mods { @@ -66,43 +68,50 @@ namespace osu.Game.Scoring if (mods != null) return mods; - if (modsJson == null) + if (localAPIMods == null) return Array.Empty(); - return getModsFromRuleset(JsonConvert.DeserializeObject(modsJson)); + var rulesetInstance = Ruleset.CreateInstance(); + return apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } set { - modsJson = null; + localAPIMods = null; mods = value; } } - private Mod[] getModsFromRuleset(DeserializedMod[] mods) => Ruleset.CreateInstance().GetAllMods().Where(mod => mods.Any(d => d.Acronym == mod.Acronym)).ToArray(); + // Used for API serialisation/deserialisation. + [JsonProperty("mods")] + [NotMapped] + private APIMod[] apiMods + { + get + { + if (localAPIMods != null) + return localAPIMods; - private string modsJson; + if (mods == null) + return Array.Empty(); + return localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); + } + set + { + localAPIMods = value; + + // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. + mods = null; + } + } + + // Used for database serialisation/deserialisation. [JsonIgnore] [Column("Mods")] public string ModsJson { - get - { - if (modsJson != null) - return modsJson; - - if (mods == null) - return null; - - return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym })); - } - set - { - modsJson = value; - - // we potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. - mods = null; - } + get => JsonConvert.SerializeObject(apiMods); + set => apiMods = JsonConvert.DeserializeObject(value); } [NotMapped] @@ -251,14 +260,6 @@ namespace osu.Game.Scoring } } - [Serializable] - protected class DeserializedMod : IMod - { - public string Acronym { get; set; } - - public bool Equals(IMod other) => Acronym == other?.Acronym; - } - public override string ToString() => $"{User} playing {Beatmap}"; public bool Equals(ScoreInfo other) diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs index a7b0fb05e3..dcf5f8a788 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs @@ -12,7 +12,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons /// /// Whether this is selected. /// - /// public readonly BindableBool Selected; /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs new file mode 100644 index 0000000000..1b9e2bda2d --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -0,0 +1,72 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using JetBrains.Annotations; +using osu.Framework.Timing; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard + { + public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) + : base(scoreProcessor, userIds) + { + } + + public void AddClock(int userId, IClock clock) + { + if (!UserScores.TryGetValue(userId, out var data)) + return; + + ((SpectatingTrackedUserData)data).Clock = clock; + } + + public void RemoveClock(int userId) + { + if (!UserScores.TryGetValue(userId, out var data)) + return; + + ((SpectatingTrackedUserData)data).Clock = null; + } + + protected override TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(userId, scoreProcessor); + + protected override void Update() + { + base.Update(); + + foreach (var (_, data) in UserScores) + data.UpdateScore(); + } + + private class SpectatingTrackedUserData : TrackedUserData + { + [CanBeNull] + public IClock Clock; + + public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor) + : base(userId, scoreProcessor) + { + } + + public override void UpdateScore() + { + if (Frames.Count == 0) + return; + + if (Clock == null) + return; + + int frameIndex = Frames.BinarySearch(new TimedFrame(Clock.CurrentTime)); + if (frameIndex < 0) + frameIndex = ~frameIndex; + frameIndex = Math.Clamp(frameIndex - 1, 0, Frames.Count - 1); + + SetFrame(Frames[frameIndex]); + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index a3d27c4e71..70de067784 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; @@ -19,9 +19,7 @@ namespace osu.Game.Screens.Play.HUD [LongRunningLoad] public class MultiplayerGameplayLeaderboard : GameplayLeaderboard { - private readonly ScoreProcessor scoreProcessor; - - private readonly Dictionary userScores = new Dictionary(); + protected readonly Dictionary UserScores = new Dictionary(); [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -32,9 +30,9 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private UserLookupCache userLookupCache { get; set; } - private Bindable scoringMode; - + private readonly ScoreProcessor scoreProcessor; private readonly BindableList playingUsers; + private Bindable scoringMode; /// /// Construct a new leaderboard. @@ -53,6 +51,8 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { + scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + foreach (var userId in playingUsers) { streamingClient.WatchUser(userId); @@ -60,19 +60,17 @@ namespace osu.Game.Screens.Play.HUD // probably won't be required in the final implementation. var resolvedUser = userLookupCache.GetUserAsync(userId).Result; - var trackedUser = new TrackedUserData(); + var trackedUser = CreateUserData(userId, scoreProcessor); + trackedUser.ScoringMode.BindTo(scoringMode); - userScores[userId] = trackedUser; var leaderboardScore = AddPlayer(resolvedUser, resolvedUser?.Id == api.LocalUser.Value.Id); + leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy); + leaderboardScore.TotalScore.BindTo(trackedUser.Score); + leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo); + leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); - ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); - ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score); - ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo); - ((IBindable)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit); + UserScores[userId] = trackedUser; } - - scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); - scoringMode.BindValueChanged(updateAllScores, true); } protected override void LoadComplete() @@ -102,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD { streamingClient.StopWatchingUser(userId); - if (userScores.TryGetValue(userId, out var trackedData)) + if (UserScores.TryGetValue(userId, out var trackedData)) trackedData.MarkUserQuit(); } @@ -110,20 +108,16 @@ namespace osu.Game.Screens.Play.HUD } } - private void updateAllScores(ValueChangedEvent mode) + private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() => { - foreach (var trackedData in userScores.Values) - trackedData.UpdateScore(scoreProcessor, mode.NewValue); - } + if (!UserScores.TryGetValue(userId, out var trackedData)) + return; - private void handleIncomingFrames(int userId, FrameDataBundle bundle) - { - if (userScores.TryGetValue(userId, out var trackedData)) - { - trackedData.LastHeader = bundle.Header; - trackedData.UpdateScore(scoreProcessor, scoringMode.Value); - } - } + trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); + trackedData.UpdateScore(); + }); + + protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor); protected override void Dispose(bool isDisposing) { @@ -140,38 +134,65 @@ namespace osu.Game.Screens.Play.HUD } } - private class TrackedUserData + protected class TrackedUserData { - public IBindableNumber Score => score; + public readonly int UserId; + public readonly ScoreProcessor ScoreProcessor; - private readonly BindableDouble score = new BindableDouble(); + public readonly BindableDouble Score = new BindableDouble(); + public readonly BindableDouble Accuracy = new BindableDouble(1); + public readonly BindableInt CurrentCombo = new BindableInt(); + public readonly BindableBool UserQuit = new BindableBool(); - public IBindableNumber Accuracy => accuracy; + public readonly IBindable ScoringMode = new Bindable(); - private readonly BindableDouble accuracy = new BindableDouble(1); + public readonly List Frames = new List(); - public IBindableNumber CurrentCombo => currentCombo; - - private readonly BindableInt currentCombo = new BindableInt(); - - public IBindable UserQuit => userQuit; - - private readonly BindableBool userQuit = new BindableBool(); - - [CanBeNull] - public FrameHeader LastHeader; - - public void MarkUserQuit() => userQuit.Value = true; - - public void UpdateScore(ScoreProcessor processor, ScoringMode mode) + public TrackedUserData(int userId, ScoreProcessor scoreProcessor) { - if (LastHeader == null) + UserId = userId; + ScoreProcessor = scoreProcessor; + + ScoringMode.BindValueChanged(_ => UpdateScore()); + } + + public void MarkUserQuit() => UserQuit.Value = true; + + public virtual void UpdateScore() + { + if (Frames.Count == 0) return; - score.Value = processor.GetImmediateScore(mode, LastHeader.MaxCombo, LastHeader.Statistics); - accuracy.Value = LastHeader.Accuracy; - currentCombo.Value = LastHeader.Combo; + SetFrame(Frames.Last()); } + + protected void SetFrame(TimedFrame frame) + { + var header = frame.Header; + + Score.Value = ScoreProcessor.GetImmediateScore(ScoringMode.Value, header.MaxCombo, header.Statistics); + Accuracy.Value = header.Accuracy; + CurrentCombo.Value = header.Combo; + } + } + + protected class TimedFrame : IComparable + { + public readonly double Time; + public readonly FrameHeader Header; + + public TimedFrame(double time) + { + Time = time; + } + + public TimedFrame(double time, FrameHeader header) + { + Time = time; + Header = header; + } + + public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time); } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 77b3d8fc3b..441c9e048a 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -226,7 +226,6 @@ namespace osu.Game.Screens.Ranking /// /// Enumerates all s contained in this . /// - /// public IEnumerable GetScorePanels() => flow.Select(t => t.Panel); /// diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index fcf20a2eb2..5ef2458919 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -189,7 +189,6 @@ namespace osu.Game.Tests.Beatmaps /// /// Creates the applicable to this . /// - /// protected abstract Ruleset CreateRuleset(); private class ConvertResult diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs index 76f97db59f..54a83f4305 100644 --- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs @@ -17,7 +17,6 @@ namespace osu.Game.Tests.Beatmaps /// /// Creates the whose legacy mod conversion is to be tested. /// - /// protected abstract Ruleset CreateRuleset(); protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) diff --git a/osu.Game/Utils/Optional.cs b/osu.Game/Utils/Optional.cs index 9f8a1c2e62..fdb7623be5 100644 --- a/osu.Game/Utils/Optional.cs +++ b/osu.Game/Utils/Optional.cs @@ -37,7 +37,6 @@ namespace osu.Game.Utils /// Shortcase for: optional.HasValue ? optional.Value : fallback. /// /// The fallback value to return if is false. - /// public T GetOr(T fallback) => HasValue ? Value : fallback; public static implicit operator Optional(T value) => new Optional(value); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 71a6f0e5cd..92e05cb4a6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index a389cc13dd..11124730c9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - +