From 079868c0bf6e23d6dc0a37d15821f7b328df4aa8 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Fri, 22 Nov 2024 16:07:16 +0000 Subject: [PATCH 1/3] Allow a `JudgementProcessor` to provide a rate-scaled time offset of a `JudgementResult` Closes https://github.com/ppy/osu/issues/30828. I'm not convinced this is the absolute cleanest solution, but I've made it in such a way that it doesn't require too much messing around with hit/judgement processing. --- osu.Game.Benchmarks/BenchmarkUnstableRate.cs | 5 +++- .../Scoring/ManiaScoreProcessor.cs | 2 ++ .../NonVisual/Ranking/UnstableRateTest.cs | 24 +++++++-------- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 20 ++++++------- ...estSceneHitEventTimingDistributionGraph.cs | 30 +++++++++---------- osu.Game/Rulesets/Scoring/HitEvent.cs | 15 ++++++---- .../Rulesets/Scoring/HitEventExtensions.cs | 10 ++----- .../Rulesets/Scoring/JudgementProcessor.cs | 15 ++++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- osu.Game/Screens/Utility/CircleGameplay.cs | 2 +- osu.Game/Screens/Utility/ScrollingGameplay.cs | 2 +- 11 files changed, 73 insertions(+), 54 deletions(-) diff --git a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs index aa229c7d06da..b6d4031f8c6d 100644 --- a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs +++ b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs @@ -19,7 +19,10 @@ public override void SetUp() events = new List(); for (int i = 0; i < 1000; i++) - events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null)); + { + double timeOffset = RNG.NextDouble(-200.0, 200.0); + events.Add(new HitEvent(timeOffset, timeOffset * RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null)); + } } [Benchmark] diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index dfd6ed6dd245..74a3ba1d50d5 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -77,6 +77,8 @@ public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary result.TimeOffset; + private class JudgementOrderComparer : IComparer { public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer(); diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 5a416d05d7ee..1a70c1f0038e 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -20,7 +20,7 @@ public class UnstableRateTest public void TestDistributedHits() { var events = Enumerable.Range(-5, 11) - .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); + .Select(t => new HitEvent(t - 5, t - 5, HitResult.Great, new HitObject(), null, null)); var unstableRate = new UnstableRate(events); @@ -33,9 +33,9 @@ public void TestMissesAndEmptyWindows() { var events = new[] { - new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null), - new HitEvent(0, 1.0, HitResult.Great, new HitObject(), null, null), - new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), + new HitEvent(-100, -100, HitResult.Miss, new HitObject(), null, null), + new HitEvent(0, 0, HitResult.Great, new HitObject(), null, null), + new HitEvent(200, 200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), }; var unstableRate = new UnstableRate(events); @@ -48,10 +48,10 @@ public void TestStaticRateChange() { var events = new[] { - new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null), - new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null), - new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null), - new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(-150, -150 * 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(-150, -150 * 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(150, 150 * 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(150, 150 * 1.5, HitResult.Great, new HitObject(), null, null), }; var unstableRate = new UnstableRate(events); @@ -64,10 +64,10 @@ public void TestDynamicRateChange() { var events = new[] { - new HitEvent(-50, 0.5, HitResult.Great, new HitObject(), null, null), - new HitEvent(75, 0.75, HitResult.Great, new HitObject(), null, null), - new HitEvent(-100, 1.0, HitResult.Great, new HitObject(), null, null), - new HitEvent(125, 1.25, HitResult.Great, new HitObject(), null, null), + new HitEvent(-50, -50 * 0.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(75, 75 * 0.75, HitResult.Great, new HitObject(), null, null), + new HitEvent(-100, -100 * 1.0, HitResult.Great, new HitObject(), null, null), + new HitEvent(125, 125 * 1.25, HitResult.Great, new HitObject(), null, null), }; var unstableRate = new UnstableRate(events); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 0f47c3cd27bd..6314ae4320c7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -64,16 +64,16 @@ public void TestNotEnoughTimedHitEvents() List hitEvents = [ // 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows - new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(30, 30, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null), + new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), ]; foreach (var ev in hitEvents) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 760210c37006..94c2a7721226 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -56,7 +56,7 @@ public void TestManyDistributedEventsOffset() [Test] public void TestAroundCentre() { - createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); + createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] @@ -64,12 +64,12 @@ public void TestSparse() { createTest(new List { - new HitEvent(-7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(-6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(-5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-7, -7, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-6, -6, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-5, -5, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(5, 5, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(6, 6, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(7, 7, HitResult.Perfect, placeholder_object, placeholder_object, null), }); } @@ -85,7 +85,7 @@ public void TestVariousTypesOfHitResult() : offset > 16 ? HitResult.Good : offset > 8 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, h.ScaledTimeOffset, result, placeholder_object, placeholder_object, null); }).ToList()); } @@ -93,7 +93,7 @@ public void TestVariousTypesOfHitResult() public void TestNonBasicHitResultsAreIgnored() { createTest(CreateDistributedHitEvents(0, 50) - .Select(h => new HitEvent(h.TimeOffset, 1.0, h.TimeOffset > 0 ? HitResult.Ok : HitResult.LargeTickHit, placeholder_object, placeholder_object, null)) + .Select(h => new HitEvent(h.TimeOffset, h.ScaledTimeOffset, h.TimeOffset > 0 ? HitResult.Ok : HitResult.LargeTickHit, placeholder_object, placeholder_object, null)) .ToList()); } @@ -110,7 +110,7 @@ public void TestMultipleWindowsOfHitResult() : offset > 8 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, h.ScaledTimeOffset, result, placeholder_object, placeholder_object, null); }); var narrow = CreateDistributedHitEvents(0, 50).Select(h => { @@ -121,7 +121,7 @@ public void TestMultipleWindowsOfHitResult() : offset > 10 ? HitResult.Good : offset > 5 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, h.ScaledTimeOffset, result, placeholder_object, placeholder_object, null); }); createTest(wide.Concat(narrow).ToList()); } @@ -129,7 +129,7 @@ public void TestMultipleWindowsOfHitResult() [Test] public void TestZeroTimeOffset() { - createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); + createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, 0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] @@ -144,9 +144,9 @@ public void TestMissesDontShow() createTest(Enumerable.Range(0, 100).Select(i => { if (i % 2 == 0) - return new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null); + return new HitEvent(0, 0, HitResult.Perfect, placeholder_object, placeholder_object, null); - return new HitEvent(30, 1.0, HitResult.Miss, placeholder_object, placeholder_object, null); + return new HitEvent(30, 30, HitResult.Miss, placeholder_object, placeholder_object, null); }).ToList()); } @@ -177,7 +177,7 @@ public static List CreateDistributedHitEvents(double centre = 0, doubl int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)) / 10; for (int j = 0; j < count; j++) - hitEvents.Add(new HitEvent(centre + i - range, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)); + hitEvents.Add(new HitEvent(centre + i - range, centre + i - range, HitResult.Perfect, placeholder_object, placeholder_object, null)); } return hitEvents; diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index 1763190899cc..d94848a3ef24 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -20,9 +20,12 @@ public readonly struct HitEvent public readonly double TimeOffset; /// - /// The true gameplay rate at the time of the event. + /// The rate-scaled time offset from the end of at which the event occurred. /// - public readonly double? GameplayRate; + /// + /// If the ruleset's hit windows are already scaled by rate, then this may be equal to . + /// + public readonly double ScaledTimeOffset; /// /// The hit result. @@ -50,15 +53,15 @@ public readonly struct HitEvent /// Creates a new . /// /// The time offset from the end of at which the event occurs. + /// The rate-scaled /// The . - /// The true gameplay rate at the time of the event. /// The that triggered the event. /// The previous . /// A position corresponding to the event. - public HitEvent(double timeOffset, double? gameplayRate, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) + public HitEvent(double timeOffset, double scaledTimeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) { TimeOffset = timeOffset; - GameplayRate = gameplayRate; + ScaledTimeOffset = scaledTimeOffset; Result = result; HitObject = hitObject; LastHitObject = lastHitObject; @@ -70,6 +73,6 @@ public HitEvent(double timeOffset, double? gameplayRate, HitResult result, HitOb /// /// The positional offset. /// The new . - public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, GameplayRate, Result, HitObject, LastHitObject, positionOffset); + public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, ScaledTimeOffset, Result, HitObject, LastHitObject, positionOffset); } } diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index fc4eef13ba29..61e766d0d022 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; namespace osu.Game.Rulesets.Scoring @@ -22,8 +21,6 @@ public static class HitEventExtensions /// public static double? CalculateUnstableRate(this IEnumerable hitEvents) { - Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); - int count = 0; double mean = 0; double sumOfSquares = 0; @@ -35,10 +32,9 @@ public static class HitEventExtensions count++; - // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. - double currentValue = e.TimeOffset / e.GameplayRate!.Value; - double nextMean = mean + (currentValue - mean) / count; - sumOfSquares += (currentValue - mean) * (currentValue - nextMean); + // ScaledTimeOffset is used to account for TimeOffset _usually_ scaling with gameplay rate. + double nextMean = mean + (e.ScaledTimeOffset - mean) / count; + sumOfSquares += (e.ScaledTimeOffset - mean) * (e.ScaledTimeOffset - nextMean); mean = nextMean; } diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 0e9033065140..6a6dfcf088bb 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; @@ -199,6 +200,20 @@ private IEnumerable enumerateRecursively(IEnumerable hitOb /// The simulated for the judgement. protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult; + /// + /// Gets the rate-scaled time offset for a . + /// + /// The judgement result. + /// The rate-scaled time offset. + /// + /// If a ruleset has rate-scaled hit windows, then this may be equal to the time offset as it is already scaled. + /// + protected virtual double GetScaledTimeOffsetForResult(JudgementResult judgementResult) + { + Debug.Assert(judgementResult.GameplayRate != null); + return judgementResult.TimeOffset / judgementResult.GameplayRate.Value; + } + protected override void Update() { base.Update(); diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 7b5af9bedaef..007b5623543a 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -274,7 +274,7 @@ protected sealed override void ApplyResultInternal(JudgementResult result) /// The to describe. /// The . protected virtual HitEvent CreateHitEvent(JudgementResult result) - => new HitEvent(result.TimeOffset, result.GameplayRate, result.Type, result.HitObject, lastHitObject, null); + => new HitEvent(result.TimeOffset, GetScaledTimeOffsetForResult(result), result.Type, result.HitObject, lastHitObject, null); protected sealed override void RevertResultInternal(JudgementResult result) { diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs index 1f970c51218b..65d025c6f2aa 100644 --- a/osu.Game/Screens/Utility/CircleGameplay.cs +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -224,7 +224,7 @@ private void attemptHit() => Schedule(() => .FadeOut(duration) .ScaleTo(1.5f, duration); - HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject + HitEvent = new HitEvent(Clock.CurrentTime - HitTime, Clock.CurrentTime - HitTime, HitResult.Good, new HitObject { HitWindows = new HitWindows(), }, null, null); diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs index 5038c53b4ae8..0794188b8988 100644 --- a/osu.Game/Screens/Utility/ScrollingGameplay.cs +++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs @@ -186,7 +186,7 @@ private void attemptHit() => Schedule(() => .FadeOut(duration / 2) .ScaleTo(1.5f, duration / 2); - HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject + HitEvent = new HitEvent(Clock.CurrentTime - HitTime, Clock.CurrentTime - HitTime, HitResult.Good, new HitObject { HitWindows = new HitWindows(), }, null, null); From 98d9568581550997c8154adbe84645ad275f1b89 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 24 Nov 2024 18:03:33 +0000 Subject: [PATCH 2/3] Use a "time scale" instead of "scaled time offset" in `HitEvent` --- osu.Game.Benchmarks/BenchmarkUnstableRate.cs | 5 +--- .../Scoring/ManiaScoreProcessor.cs | 2 +- .../NonVisual/Ranking/UnstableRateTest.cs | 24 +++++++-------- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 20 ++++++------- ...estSceneHitEventTimingDistributionGraph.cs | 30 +++++++++---------- osu.Game/Rulesets/Scoring/HitEvent.cs | 15 ++++------ .../Rulesets/Scoring/HitEventExtensions.cs | 7 +++-- .../Rulesets/Scoring/JudgementProcessor.cs | 11 +++---- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- osu.Game/Screens/Utility/CircleGameplay.cs | 2 +- osu.Game/Screens/Utility/ScrollingGameplay.cs | 2 +- 11 files changed, 56 insertions(+), 64 deletions(-) diff --git a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs index b6d4031f8c6d..aa229c7d06da 100644 --- a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs +++ b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs @@ -19,10 +19,7 @@ public override void SetUp() events = new List(); for (int i = 0; i < 1000; i++) - { - double timeOffset = RNG.NextDouble(-200.0, 200.0); - events.Add(new HitEvent(timeOffset, timeOffset * RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null)); - } + events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null)); } [Benchmark] diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 74a3ba1d50d5..77c2186e8027 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -77,7 +77,7 @@ public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary result.TimeOffset; + protected override double GetTimeScaleForResult(JudgementResult result) => 1.0; private class JudgementOrderComparer : IComparer { diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 1a70c1f0038e..5a416d05d7ee 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -20,7 +20,7 @@ public class UnstableRateTest public void TestDistributedHits() { var events = Enumerable.Range(-5, 11) - .Select(t => new HitEvent(t - 5, t - 5, HitResult.Great, new HitObject(), null, null)); + .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); var unstableRate = new UnstableRate(events); @@ -33,9 +33,9 @@ public void TestMissesAndEmptyWindows() { var events = new[] { - new HitEvent(-100, -100, HitResult.Miss, new HitObject(), null, null), - new HitEvent(0, 0, HitResult.Great, new HitObject(), null, null), - new HitEvent(200, 200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), + new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null), + new HitEvent(0, 1.0, HitResult.Great, new HitObject(), null, null), + new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), }; var unstableRate = new UnstableRate(events); @@ -48,10 +48,10 @@ public void TestStaticRateChange() { var events = new[] { - new HitEvent(-150, -150 * 1.5, HitResult.Great, new HitObject(), null, null), - new HitEvent(-150, -150 * 1.5, HitResult.Great, new HitObject(), null, null), - new HitEvent(150, 150 * 1.5, HitResult.Great, new HitObject(), null, null), - new HitEvent(150, 150 * 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null), }; var unstableRate = new UnstableRate(events); @@ -64,10 +64,10 @@ public void TestDynamicRateChange() { var events = new[] { - new HitEvent(-50, -50 * 0.5, HitResult.Great, new HitObject(), null, null), - new HitEvent(75, 75 * 0.75, HitResult.Great, new HitObject(), null, null), - new HitEvent(-100, -100 * 1.0, HitResult.Great, new HitObject(), null, null), - new HitEvent(125, 125 * 1.25, HitResult.Great, new HitObject(), null, null), + new HitEvent(-50, 0.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(75, 0.75, HitResult.Great, new HitObject(), null, null), + new HitEvent(-100, 1.0, HitResult.Great, new HitObject(), null, null), + new HitEvent(125, 1.25, HitResult.Great, new HitObject(), null, null), }; var unstableRate = new UnstableRate(events); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 6314ae4320c7..0f47c3cd27bd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -64,16 +64,16 @@ public void TestNotEnoughTimedHitEvents() List hitEvents = [ // 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows - new HitEvent(30, 30, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null), - new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 0, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null), + new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), + new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), ]; foreach (var ev in hitEvents) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 94c2a7721226..d23f5d05bb98 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -56,7 +56,7 @@ public void TestManyDistributedEventsOffset() [Test] public void TestAroundCentre() { - createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); + createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] @@ -64,12 +64,12 @@ public void TestSparse() { createTest(new List { - new HitEvent(-7, -7, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(-6, -6, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(-5, -5, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(5, 5, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(6, 6, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(7, 7, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), }); } @@ -85,7 +85,7 @@ public void TestVariousTypesOfHitResult() : offset > 16 ? HitResult.Good : offset > 8 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, h.ScaledTimeOffset, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, h.TimeScale, result, placeholder_object, placeholder_object, null); }).ToList()); } @@ -93,7 +93,7 @@ public void TestVariousTypesOfHitResult() public void TestNonBasicHitResultsAreIgnored() { createTest(CreateDistributedHitEvents(0, 50) - .Select(h => new HitEvent(h.TimeOffset, h.ScaledTimeOffset, h.TimeOffset > 0 ? HitResult.Ok : HitResult.LargeTickHit, placeholder_object, placeholder_object, null)) + .Select(h => new HitEvent(h.TimeOffset, h.TimeScale, h.TimeOffset > 0 ? HitResult.Ok : HitResult.LargeTickHit, placeholder_object, placeholder_object, null)) .ToList()); } @@ -110,7 +110,7 @@ public void TestMultipleWindowsOfHitResult() : offset > 8 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, h.ScaledTimeOffset, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, h.TimeScale, result, placeholder_object, placeholder_object, null); }); var narrow = CreateDistributedHitEvents(0, 50).Select(h => { @@ -121,7 +121,7 @@ public void TestMultipleWindowsOfHitResult() : offset > 10 ? HitResult.Good : offset > 5 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, h.ScaledTimeOffset, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, h.TimeScale, result, placeholder_object, placeholder_object, null); }); createTest(wide.Concat(narrow).ToList()); } @@ -129,7 +129,7 @@ public void TestMultipleWindowsOfHitResult() [Test] public void TestZeroTimeOffset() { - createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, 0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); + createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] @@ -144,9 +144,9 @@ public void TestMissesDontShow() createTest(Enumerable.Range(0, 100).Select(i => { if (i % 2 == 0) - return new HitEvent(0, 0, HitResult.Perfect, placeholder_object, placeholder_object, null); + return new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null); - return new HitEvent(30, 30, HitResult.Miss, placeholder_object, placeholder_object, null); + return new HitEvent(30, 1.0, HitResult.Miss, placeholder_object, placeholder_object, null); }).ToList()); } @@ -177,7 +177,7 @@ public static List CreateDistributedHitEvents(double centre = 0, doubl int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)) / 10; for (int j = 0; j < count; j++) - hitEvents.Add(new HitEvent(centre + i - range, centre + i - range, HitResult.Perfect, placeholder_object, placeholder_object, null)); + hitEvents.Add(new HitEvent(centre + i - range, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)); } return hitEvents; diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index d94848a3ef24..d3b6a23f18bc 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -20,12 +20,9 @@ public readonly struct HitEvent public readonly double TimeOffset; /// - /// The rate-scaled time offset from the end of at which the event occurred. + /// The scale to apply to the to account for potential rate adjustments. /// - /// - /// If the ruleset's hit windows are already scaled by rate, then this may be equal to . - /// - public readonly double ScaledTimeOffset; + public readonly double TimeScale; /// /// The hit result. @@ -53,15 +50,15 @@ public readonly struct HitEvent /// Creates a new . /// /// The time offset from the end of at which the event occurs. - /// The rate-scaled + /// The rate-scaled /// The . /// The that triggered the event. /// The previous . /// A position corresponding to the event. - public HitEvent(double timeOffset, double scaledTimeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) + public HitEvent(double timeOffset, double timeScale, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) { TimeOffset = timeOffset; - ScaledTimeOffset = scaledTimeOffset; + TimeScale = timeScale; Result = result; HitObject = hitObject; LastHitObject = lastHitObject; @@ -73,6 +70,6 @@ public HitEvent(double timeOffset, double scaledTimeOffset, HitResult result, Hi /// /// The positional offset. /// The new . - public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, ScaledTimeOffset, Result, HitObject, LastHitObject, positionOffset); + public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, TimeScale, Result, HitObject, LastHitObject, positionOffset); } } diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 61e766d0d022..1058e8aa08be 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -32,9 +32,10 @@ public static class HitEventExtensions count++; - // ScaledTimeOffset is used to account for TimeOffset _usually_ scaling with gameplay rate. - double nextMean = mean + (e.ScaledTimeOffset - mean) / count; - sumOfSquares += (e.ScaledTimeOffset - mean) * (e.ScaledTimeOffset - nextMean); + // Division by TimeScale is used to account for TimeOffset _usually_ scaling with gameplay rate. + double currentValue = e.TimeOffset / e.TimeScale; + double nextMean = mean + (currentValue - mean) / count; + sumOfSquares += (currentValue - mean) * (currentValue - nextMean); mean = nextMean; } diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 6a6dfcf088bb..97192f82aa9d 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -201,17 +201,14 @@ private IEnumerable enumerateRecursively(IEnumerable hitOb protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult; /// - /// Gets the rate-scaled time offset for a . + /// Gets the time scale for a . /// /// The judgement result. - /// The rate-scaled time offset. - /// - /// If a ruleset has rate-scaled hit windows, then this may be equal to the time offset as it is already scaled. - /// - protected virtual double GetScaledTimeOffsetForResult(JudgementResult judgementResult) + /// The time scale. + protected virtual double GetTimeScaleForResult(JudgementResult judgementResult) { Debug.Assert(judgementResult.GameplayRate != null); - return judgementResult.TimeOffset / judgementResult.GameplayRate.Value; + return judgementResult.GameplayRate.Value; } protected override void Update() diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 007b5623543a..892823d534d1 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -274,7 +274,7 @@ protected sealed override void ApplyResultInternal(JudgementResult result) /// The to describe. /// The . protected virtual HitEvent CreateHitEvent(JudgementResult result) - => new HitEvent(result.TimeOffset, GetScaledTimeOffsetForResult(result), result.Type, result.HitObject, lastHitObject, null); + => new HitEvent(result.TimeOffset, GetTimeScaleForResult(result), result.Type, result.HitObject, lastHitObject, null); protected sealed override void RevertResultInternal(JudgementResult result) { diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs index 65d025c6f2aa..1f970c51218b 100644 --- a/osu.Game/Screens/Utility/CircleGameplay.cs +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -224,7 +224,7 @@ private void attemptHit() => Schedule(() => .FadeOut(duration) .ScaleTo(1.5f, duration); - HitEvent = new HitEvent(Clock.CurrentTime - HitTime, Clock.CurrentTime - HitTime, HitResult.Good, new HitObject + HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { HitWindows = new HitWindows(), }, null, null); diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs index 0794188b8988..5038c53b4ae8 100644 --- a/osu.Game/Screens/Utility/ScrollingGameplay.cs +++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs @@ -186,7 +186,7 @@ private void attemptHit() => Schedule(() => .FadeOut(duration / 2) .ScaleTo(1.5f, duration / 2); - HitEvent = new HitEvent(Clock.CurrentTime - HitTime, Clock.CurrentTime - HitTime, HitResult.Good, new HitObject + HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { HitWindows = new HitWindows(), }, null, null); From a26c94f65dee008bdc8635fa4229e9f7284e6d47 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 24 Nov 2024 18:06:02 +0000 Subject: [PATCH 3/3] Make `TimeScale` nullable similarly to old `GameplayRate` logic --- osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs | 2 +- osu.Game/Rulesets/Scoring/HitEvent.cs | 4 ++-- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 5 ++++- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 7 +------ 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 77c2186e8027..d84bc46fefd6 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -77,7 +77,7 @@ public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary 1.0; + protected override double? GetTimeScaleForResult(JudgementResult result) => 1.0; private class JudgementOrderComparer : IComparer { diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index d3b6a23f18bc..319242ee7944 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -22,7 +22,7 @@ public readonly struct HitEvent /// /// The scale to apply to the to account for potential rate adjustments. /// - public readonly double TimeScale; + public readonly double? TimeScale; /// /// The hit result. @@ -55,7 +55,7 @@ public readonly struct HitEvent /// The that triggered the event. /// The previous . /// A position corresponding to the event. - public HitEvent(double timeOffset, double timeScale, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) + public HitEvent(double timeOffset, double? timeScale, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) { TimeOffset = timeOffset; TimeScale = timeScale; diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 1058e8aa08be..74db181a29df 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace osu.Game.Rulesets.Scoring @@ -21,6 +22,8 @@ public static class HitEventExtensions /// public static double? CalculateUnstableRate(this IEnumerable hitEvents) { + Debug.Assert(hitEvents.All(ev => ev.TimeScale != null)); + int count = 0; double mean = 0; double sumOfSquares = 0; @@ -33,7 +36,7 @@ public static class HitEventExtensions count++; // Division by TimeScale is used to account for TimeOffset _usually_ scaling with gameplay rate. - double currentValue = e.TimeOffset / e.TimeScale; + double currentValue = e.TimeOffset / e.TimeScale!.Value; double nextMean = mean + (currentValue - mean) / count; sumOfSquares += (currentValue - mean) * (currentValue - nextMean); mean = nextMean; diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 97192f82aa9d..a210418b8aa1 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; @@ -205,11 +204,7 @@ private IEnumerable enumerateRecursively(IEnumerable hitOb /// /// The judgement result. /// The time scale. - protected virtual double GetTimeScaleForResult(JudgementResult judgementResult) - { - Debug.Assert(judgementResult.GameplayRate != null); - return judgementResult.GameplayRate.Value; - } + protected virtual double? GetTimeScaleForResult(JudgementResult judgementResult) => judgementResult.GameplayRate; protected override void Update() {