From f49481e308b68176e560a283870d6dbcbd43c581 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Mar 2021 12:48:21 +0300 Subject: [PATCH 01/23] Add old skin spinner SPM background for testing --- .../Resources/old-skin/spinner-rpm.png | Bin 0 -> 10583 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png 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 0000000000000000000000000000000000000000..73753554f74fab988c7e5d8c7722d22928120682 GIT binary patch literal 10583 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 Date: Fri, 26 Mar 2021 13:09:44 +0300 Subject: [PATCH 02/23] Refactor spinner SPM counter for skinning purposes --- .../Mods/TestSceneOsuModAutoplay.cs | 10 +-- .../Mods/TestSceneOsuModSpunOut.cs | 4 +- .../TestSceneSpinnerRotation.cs | 10 +-- .../Objects/Drawables/DrawableSpinner.cs | 48 +++++------- .../Skinning/Default/DefaultSpinner.cs | 64 ++++++++++++++++ ...rSpmCounter.cs => SpinnerSpmCalculator.cs} | 74 +++++-------------- 6 files changed, 114 insertions(+), 96 deletions(-) rename osu.Game.Rulesets.Osu/Skinning/Default/{SpinnerSpmCounter.cs => SpinnerSpmCalculator.cs} (61%) 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/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..1a89fc308e 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,7 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + AddInternal(spmCalculator = new SpinnerSpmCalculator + { + Result = { BindTarget = SpinsPerMinute }, + }); + + AddRangeInternal(new Drawable[] { ticks = new Container(), new AspectContainer @@ -77,20 +88,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 +165,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 +275,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; + } } } From f848ef534747babe4c020e3b1a759b2fc42d259d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Mar 2021 13:10:04 +0300 Subject: [PATCH 03/23] Add legacy spinner SPM counter support --- .../Skinning/Legacy/LegacySpinner.cs | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) 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: From 9504fe3f3c1fb78b8b4f6510ec988933f26bd71d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 13:43:05 +0900 Subject: [PATCH 04/23] Inline add of spm calculation (no need for it to be a separate call) --- .../Objects/Drawables/DrawableSpinner.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 1a89fc308e..3a4753761a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -69,13 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; - AddInternal(spmCalculator = new SpinnerSpmCalculator - { - Result = { BindTarget = SpinsPerMinute }, - }); - AddRangeInternal(new Drawable[] { + spmCalculator = new SpinnerSpmCalculator + { + Result = { BindTarget = SpinsPerMinute }, + }, ticks = new Container(), new AspectContainer { From bb15baf118f6e49306acc9688cd6790b39dc102c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 17:31:14 +0900 Subject: [PATCH 05/23] Add initial multiplayer spectator leaderboard --- .../MultiplayerSpectatorLeaderboard.cs | 82 +++++++++++++++++++ .../HUD/MultiplayerGameplayLeaderboard.cs | 19 +++-- 2 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs 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..35c3471ce2 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -0,0 +1,82 @@ +// 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.Timing; +using osu.Game.Online.Spectator; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard + { + private readonly Dictionary trackedData = new Dictionary(); + + public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) + : base(scoreProcessor, userIds) + { + } + + public void AddSource(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); + + public void RemoveSource(int userId, IClock source) => trackedData.Remove(userId); + + protected override void OnIncomingFrames(int userId, FrameDataBundle bundle) + { + if (!trackedData.TryGetValue(userId, out var data)) + return; + + data.Frames.Add(new TimedFrameHeader(bundle.Frames.First().Time, bundle.Header)); + } + + protected override void Update() + { + base.Update(); + + foreach (var (userId, data) in trackedData) + { + var targetTime = data.Clock.CurrentTime; + + int frameIndex = data.Frames.BinarySearch(new TimedFrameHeader(targetTime)); + if (frameIndex < 0) + frameIndex = ~frameIndex; + frameIndex = Math.Clamp(frameIndex - 1, 0, data.Frames.Count - 1); + + SetCurrentFrame(userId, data.Frames[frameIndex].Header); + } + } + + private class TrackedUserData + { + public readonly IClock Clock; + public readonly List Frames = new List(); + + public TrackedUserData(IClock clock) + { + Clock = clock; + } + } + + private class TimedFrameHeader : IComparable + { + public readonly double Time; + public readonly FrameHeader Header; + + public TimedFrameHeader(double time) + { + Time = time; + } + + public TimedFrameHeader(double time, FrameHeader header) + { + Time = time; + Header = header; + } + + public int CompareTo(TimedFrameHeader other) => Time.CompareTo(other.Time); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index a3d27c4e71..65ad8be3f0 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -20,7 +20,6 @@ namespace osu.Game.Screens.Play.HUD public class MultiplayerGameplayLeaderboard : GameplayLeaderboard { private readonly ScoreProcessor scoreProcessor; - private readonly Dictionary userScores = new Dictionary(); [Resolved] @@ -116,13 +115,19 @@ namespace osu.Game.Screens.Play.HUD trackedData.UpdateScore(scoreProcessor, mode.NewValue); } - private void handleIncomingFrames(int userId, FrameDataBundle bundle) + private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() => { - if (userScores.TryGetValue(userId, out var trackedData)) - { - trackedData.LastHeader = bundle.Header; - trackedData.UpdateScore(scoreProcessor, scoringMode.Value); - } + if (userScores.ContainsKey(userId)) + OnIncomingFrames(userId, bundle); + }); + + protected virtual void OnIncomingFrames(int userId, FrameDataBundle bundle) => SetCurrentFrame(userId, bundle.Header); + + protected void SetCurrentFrame(int userId, FrameHeader header) + { + var trackedScore = userScores[userId]; + trackedScore.LastHeader = header; + trackedScore.UpdateScore(scoreProcessor, scoringMode.Value); } protected override void Dispose(bool isDisposing) From 3b86f0eb2f55839c4301cba1aefcc971ba205149 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:15:23 +0900 Subject: [PATCH 06/23] Fix exception with 0 frames --- .../Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs index 35c3471ce2..08db5befa4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -40,6 +40,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { var targetTime = data.Clock.CurrentTime; + if (data.Frames.Count == 0) + continue; + int frameIndex = data.Frames.BinarySearch(new TimedFrameHeader(targetTime)); if (frameIndex < 0) frameIndex = ~frameIndex; From 90e243eea5404e1632fc3f63c679c8fc38874c60 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:15:27 +0900 Subject: [PATCH 07/23] Rename methods --- .../Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs index 08db5befa4..f16869b43d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -20,9 +20,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } - public void AddSource(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); + public void AddClock(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); - public void RemoveSource(int userId, IClock source) => trackedData.Remove(userId); + public void RemoveClock(int userId, IClock source) => trackedData.Remove(userId); protected override void OnIncomingFrames(int userId, FrameDataBundle bundle) { From 589ce4bdc2b09c7f96d99582aea7e7f8ef430eb8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:16:10 +0900 Subject: [PATCH 08/23] Add test --- ...estSceneMultiplayerSpectatorLeaderboard.cs | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs new file mode 100644 index 0000000000..57e9fb37cc --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -0,0 +1,207 @@ +// 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 100 frames in sets of 1. + // For user 56, send 100 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); + + setTime(500); + assertCombo(55, 5); + assertCombo(56, 10); + + setTime(1100); + assertCombo(55, 11); + assertCombo(56, 20); + } + + private void setTime(double time) => AddStep($"set time {time}", () => + { + foreach (var (_, clock) in clocks) + clock.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}" + }); + } + } + } +} From b49997f531dad5e80f50fa6cbe1f4349a99be38f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:18:23 +0900 Subject: [PATCH 09/23] Add test for no frames --- .../TestSceneMultiplayerSpectatorLeaderboard.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs index 57e9fb37cc..53efb4392f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -118,6 +118,13 @@ namespace osu.Game.Tests.Visual.Multiplayer assertCombo(56, 20); } + [Test] + public void TestNoFrames() + { + assertCombo(55, 0); + assertCombo(56, 1); + } + private void setTime(double time) => AddStep($"set time {time}", () => { foreach (var (_, clock) in clocks) From 9ddcd686ac09e3f0aa35c0f5a9c9ae01e7e44f0e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:23:38 +0900 Subject: [PATCH 10/23] Fix incorrect assert --- .../Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs index 53efb4392f..8589cffcc9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestNoFrames() { assertCombo(55, 0); - assertCombo(56, 1); + assertCombo(56, 0); } private void setTime(double time) => AddStep($"set time {time}", () => From e73f3f52d7730854a9f360dc2f1ae7df0ec834ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:23:41 +0900 Subject: [PATCH 11/23] Add some more asserts --- ...estSceneMultiplayerSpectatorLeaderboard.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs index 8589cffcc9..3b2cfb1c7b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -95,8 +95,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("send frames", () => { - // For user 55, send 100 frames in sets of 1. - // For user 56, send 100 frames in sets of 10. + // 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); @@ -109,13 +109,25 @@ namespace osu.Game.Tests.Visual.Multiplayer 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] @@ -131,6 +143,9 @@ namespace osu.Game.Tests.Visual.Multiplayer 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); From 7cbc8f2695b4868fa4be7a9b41ebd8ef42b1b0d4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:29:02 +0900 Subject: [PATCH 12/23] Add some xmldocs --- .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 65ad8be3f0..f0d2ac4b7f 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -121,8 +121,21 @@ namespace osu.Game.Screens.Play.HUD OnIncomingFrames(userId, bundle); }); + /// + /// Invoked when new frames have arrived for a user. + /// + /// + /// By default, this immediately sets the current frame to be displayed for the user. + /// + /// The user which the frames arrived for. + /// The bundle of frames. protected virtual void OnIncomingFrames(int userId, FrameDataBundle bundle) => SetCurrentFrame(userId, bundle.Header); + /// + /// Sets the current frame to be displayed for a user. + /// + /// The user to set the frame of. + /// The frame to set. protected void SetCurrentFrame(int userId, FrameHeader header) { var trackedScore = userScores[userId]; From d2c37e6cf8828e5793c9aef36b76fd6ce986b537 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:41:58 +0900 Subject: [PATCH 13/23] Remove unnecessary parameter --- .../Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs index f16869b43d..aa9f162036 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void AddClock(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); - public void RemoveClock(int userId, IClock source) => trackedData.Remove(userId); + public void RemoveClock(int userId) => trackedData.Remove(userId); protected override void OnIncomingFrames(int userId, FrameDataBundle bundle) { From 4fcddfb44b7284dcf39720a685d2f34813e56ecd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 13:42:14 +0900 Subject: [PATCH 14/23] Fix multiplayer test failure --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } } } From b5954a55adc3af47c27ea4c2b354e320d9b6cf4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 17:46:14 +0900 Subject: [PATCH 15/23] Remove empty xmldoc --- osu.Desktop/Program.cs | 1 - .../Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs | 1 - osu.Game.Tournament/TournamentGameBase.cs | 1 - osu.Game/Beatmaps/IBeatmap.cs | 1 - osu.Game/Configuration/SettingsStore.cs | 1 - osu.Game/Graphics/Backgrounds/Triangles.cs | 1 - osu.Game/IO/Serialization/IJsonSerializable.cs | 1 - osu.Game/Input/KeyBindingStore.cs | 1 - osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 1 - osu.Game/Rulesets/Objects/SliderPath.cs | 2 -- osu.Game/Rulesets/Ruleset.cs | 1 - osu.Game/Rulesets/Scoring/HitWindows.cs | 1 - osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs | 1 - osu.Game/Screens/Ranking/ScorePanelList.cs | 1 - osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs | 1 - osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs | 1 - osu.Game/Utils/Optional.cs | 1 - 17 files changed, 18 deletions(-) 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.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/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/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); From 905cd7c8eb9e7c3785ffa88e63cce5b4b7f2c024 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 19:22:07 +0900 Subject: [PATCH 16/23] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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.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 @@ - + From d2d7f77430181ce55d76eb7e2ef24613cfe69ca4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 19:50:24 +0900 Subject: [PATCH 17/23] Fix mods not being serialised correctly in ScoreInfo --- .../Online/TestAPIModJsonSerialization.cs | 33 +++++++++++++ osu.Game/Scoring/ScoreInfo.cs | 49 +++++++++---------- 2 files changed, 57 insertions(+), 25 deletions(-) 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/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index ef11c19e3f..df8f309ee0 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,45 +68,50 @@ namespace osu.Game.Scoring if (mods != null) return mods; - if (modsJson == null) + if (apiMods == 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(); - - private string modsJson; - - [JsonIgnore] - [Column("Mods")] - public string ModsJson + // Used for API serialisation/deserialisation. + [JsonProperty("mods")] + private APIMod[] apiMods { get { - if (modsJson != null) - return modsJson; + if (localAPIMods != null) + return localAPIMods; if (mods == null) - return null; + return Array.Empty(); - return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym })); + return localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); } set { - modsJson = value; + localAPIMods = value; - // we potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. + // 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. + [Column("Mods")] + private string modsString + { + get => JsonConvert.SerializeObject(apiMods); + set => apiMods = JsonConvert.DeserializeObject(value); + } + [NotMapped] [JsonProperty("user")] public User User { get; set; } @@ -251,14 +258,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) From 982d8fa8b1ef8ce69a4456b8a31192f948314f09 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 20:49:26 +0900 Subject: [PATCH 18/23] Fix incorrect reference --- osu.Game/Scoring/ScoreInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index df8f309ee0..1e438edeea 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -68,7 +68,7 @@ namespace osu.Game.Scoring if (mods != null) return mods; - if (apiMods == null) + if (localAPIMods == null) return Array.Empty(); var rulesetInstance = Ruleset.CreateInstance(); From 625484468e4ba788e04b73b0c6b55614d8e30bdf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 20:49:37 +0900 Subject: [PATCH 19/23] Fix DB serialisation --- osu.Game/Scoring/ScoreInfo.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 1e438edeea..01b62f8b4f 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -105,8 +105,9 @@ namespace osu.Game.Scoring } // Used for database serialisation/deserialisation. + [JsonIgnore] [Column("Mods")] - private string modsString + public string ModsString { get => JsonConvert.SerializeObject(apiMods); set => apiMods = JsonConvert.DeserializeObject(value); From 8413b0a5d32a7aa159bfc971f10b2b11bd058c5c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 20:49:44 +0900 Subject: [PATCH 20/23] Don't map api mods to DB --- osu.Game/Scoring/ScoreInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 01b62f8b4f..312b5f46f4 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -83,6 +83,7 @@ namespace osu.Game.Scoring // Used for API serialisation/deserialisation. [JsonProperty("mods")] + [NotMapped] private APIMod[] apiMods { get From e9a114a15c37bf498ff7e1e8b24775964f01a4b8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 20:50:18 +0900 Subject: [PATCH 21/23] Rename property back --- osu.Game/Scoring/ScoreInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 312b5f46f4..222f69b025 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -108,7 +108,7 @@ namespace osu.Game.Scoring // Used for database serialisation/deserialisation. [JsonIgnore] [Column("Mods")] - public string ModsString + public string ModsJson { get => JsonConvert.SerializeObject(apiMods); set => apiMods = JsonConvert.DeserializeObject(value); From c531e38a369452f3be8664e0c2b6ff4bb079e64c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 22:00:27 +0900 Subject: [PATCH 22/23] Rework to create a derived tracked user data instead --- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- .../MultiplayerSpectatorLeaderboard.cs | 85 +++++------ .../HUD/MultiplayerGameplayLeaderboard.cs | 137 +++++++++--------- 3 files changed, 107 insertions(+), 117 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 1ee848b902..272d547505 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -163,7 +163,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/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs index aa9f162036..1b9e2bda2d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -2,10 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; +using JetBrains.Annotations; using osu.Framework.Timing; -using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -13,73 +11,62 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard { - private readonly Dictionary trackedData = new Dictionary(); - public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) : base(scoreProcessor, userIds) { } - public void AddClock(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); - - public void RemoveClock(int userId) => trackedData.Remove(userId); - - protected override void OnIncomingFrames(int userId, FrameDataBundle bundle) + public void AddClock(int userId, IClock clock) { - if (!trackedData.TryGetValue(userId, out var data)) + if (!UserScores.TryGetValue(userId, out var data)) return; - data.Frames.Add(new TimedFrameHeader(bundle.Frames.First().Time, bundle.Header)); + ((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 (userId, data) in trackedData) + foreach (var (_, data) in UserScores) + data.UpdateScore(); + } + + private class SpectatingTrackedUserData : TrackedUserData + { + [CanBeNull] + public IClock Clock; + + public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor) + : base(userId, scoreProcessor) { - var targetTime = data.Clock.CurrentTime; + } - if (data.Frames.Count == 0) - continue; + public override void UpdateScore() + { + if (Frames.Count == 0) + return; - int frameIndex = data.Frames.BinarySearch(new TimedFrameHeader(targetTime)); + if (Clock == null) + return; + + int frameIndex = Frames.BinarySearch(new TimedFrame(Clock.CurrentTime)); if (frameIndex < 0) frameIndex = ~frameIndex; - frameIndex = Math.Clamp(frameIndex - 1, 0, data.Frames.Count - 1); + frameIndex = Math.Clamp(frameIndex - 1, 0, Frames.Count - 1); - SetCurrentFrame(userId, data.Frames[frameIndex].Header); + SetFrame(Frames[frameIndex]); } } - - private class TrackedUserData - { - public readonly IClock Clock; - public readonly List Frames = new List(); - - public TrackedUserData(IClock clock) - { - Clock = clock; - } - } - - private class TimedFrameHeader : IComparable - { - public readonly double Time; - public readonly FrameHeader Header; - - public TimedFrameHeader(double time) - { - Time = time; - } - - public TimedFrameHeader(double time, FrameHeader header) - { - Time = time; - Header = header; - } - - public int CompareTo(TimedFrameHeader other) => Time.CompareTo(other.Time); - } } } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index f0d2ac4b7f..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,8 +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; } @@ -31,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. @@ -52,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); @@ -59,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() @@ -101,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(); } @@ -109,39 +108,16 @@ namespace osu.Game.Screens.Play.HUD } } - private void updateAllScores(ValueChangedEvent mode) - { - foreach (var trackedData in userScores.Values) - trackedData.UpdateScore(scoreProcessor, mode.NewValue); - } - private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() => { - if (userScores.ContainsKey(userId)) - OnIncomingFrames(userId, bundle); + if (!UserScores.TryGetValue(userId, out var trackedData)) + return; + + trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); + trackedData.UpdateScore(); }); - /// - /// Invoked when new frames have arrived for a user. - /// - /// - /// By default, this immediately sets the current frame to be displayed for the user. - /// - /// The user which the frames arrived for. - /// The bundle of frames. - protected virtual void OnIncomingFrames(int userId, FrameDataBundle bundle) => SetCurrentFrame(userId, bundle.Header); - - /// - /// Sets the current frame to be displayed for a user. - /// - /// The user to set the frame of. - /// The frame to set. - protected void SetCurrentFrame(int userId, FrameHeader header) - { - var trackedScore = userScores[userId]; - trackedScore.LastHeader = header; - trackedScore.UpdateScore(scoreProcessor, scoringMode.Value); - } + protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor); protected override void Dispose(bool isDisposing) { @@ -158,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); } } } From 1e002841cff4ffeac18a58c57c6a0769e33b05c9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 22:03:45 +0900 Subject: [PATCH 23/23] Add test for scoring mode changes --- .../TestSceneMultiplayerGameplayLeaderboard.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 272d547505..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;