From 8b542e5442f42ad132af0414a4f7191df9718a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 9 May 2025 10:43:44 +0200 Subject: [PATCH] Refactor hit windows class structure to reduce rigidity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change pulls back a significant degree of overspecialisation and rigidity in the class structure of `HitWindows` to make subsequent changes to hit windows, whose purpose is to improve replay playback accuracy, possible to do cleanly. Notably: - `HitWindows` is full abstract now. In a few use cases, and as a reference for ruleset implementors, `DefaultHitWindows` is provided as a separate class instead. This fixes the weirdness wherein `HitWindows` always declared 6 fields for result types but some of them would never be set to a non-zero value or read. - `HitWindow.GetRanges()` is deleted because it is overspecialised and prevents being able to adjust hitwindows by ±0.5ms cleanly which will be required later. The fallout of this is that the assertion that used `GetRanges()` in the `HitWindows` ctor must use something else now, and the closest thing to it was `GetAllAvailableWindows()`, which didn't return the miss window - so I made it return the miss window and fixed the one consumer that didn't want it (bar hit error meter) to skip it. - Diff also contains some clean-up around `DifficultyRange` to unify handling of it. --- .../Scoring/ManiaHitWindows.cs | 59 +++++++- .../TestSceneStartTimeOrderedHitPolicy.cs | 23 +++- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 4 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 5 +- .../Scoring/OsuHitWindows.cs | 44 ++++-- .../Judgements/TestSceneHitJudgements.cs | 2 +- .../Scoring/TaikoHitWindows.cs | 40 +++++- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 5 +- .../TestSceneDrainingHealthProcessor.cs | 2 +- .../NonVisual/FirstAvailableHitWindowsTest.cs | 8 +- .../TestSceneGameplaySampleTriggerSource.cs | 10 +- osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs | 22 ++- osu.Game/Rulesets/Objects/HitObject.cs | 2 +- .../Rulesets/Scoring/DefaultHitWindows.cs | 66 +++++++++ osu.Game/Rulesets/Scoring/HitWindows.cs | 126 ++---------------- .../HUD/HitErrorMeters/BarHitErrorMeter.cs | 2 +- osu.Game/Screens/Utility/CircleGameplay.cs | 2 +- osu.Game/Screens/Utility/ScrollingGameplay.cs | 2 +- 18 files changed, 254 insertions(+), 170 deletions(-) create mode 100644 osu.Game/Rulesets/Scoring/DefaultHitWindows.cs diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index 627f48f391..c0ba03d8ed 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -1,15 +1,30 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; +using System; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { public class ManiaHitWindows : HitWindows { + private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D); + private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34); + private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67); + private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97); + private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121); + private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158); + private readonly double multiplier; + private double perfect; + private double great; + private double good; + private double ok; + private double meh; + private double miss; + public ManiaHitWindows() : this(1) { @@ -36,11 +51,41 @@ namespace osu.Game.Rulesets.Mania.Scoring return false; } - protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r => - new DifficultyRange( - r.Result, - r.Min * multiplier, - r.Average * multiplier, - r.Max * multiplier)).ToArray(); + public override void SetDifficulty(double difficulty) + { + perfect = IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) * multiplier; + great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range) * multiplier; + good = IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range) * multiplier; + ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range) * multiplier; + meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range) * multiplier; + miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range) * multiplier; + } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Perfect: + return perfect; + + case HitResult.Great: + return great; + + case HitResult.Good: + return good; + + case HitResult.Ok: + return ok; + + case HitResult.Meh: + return meh; + + case HitResult.Miss: + return miss; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 895e9bbdee..c637ed45f7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -476,15 +476,24 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestHitWindows : HitWindows { - private static readonly DifficultyRange[] ranges = - { - new DifficultyRange(HitResult.Great, 500, 500, 500), - new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window), - }; - public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; - protected override DifficultyRange[] GetRanges() => ranges; + public override void SetDifficulty(double difficulty) { } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Great: + return 500; + + case HitResult.Miss: + return early_miss_window; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } } private partial class ScoreAccessibleReplayPlayer : ReplayPlayer diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index e3dfe8e69a..31d00a2610 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -21,12 +21,12 @@ namespace osu.Game.Rulesets.Osu.Objects /// /// The RPM required to clear the spinner at ODs [ 0, 5, 10 ]. /// - private static readonly (int min, int mid, int max) clear_rpm_range = (90, 150, 225); + private static readonly DifficultyRange clear_rpm_range = new DifficultyRange(90, 150, 225); /// /// The RPM required to complete the spinner and receive full score at ODs [ 0, 5, 10 ]. /// - private static readonly (int min, int mid, int max) complete_rpm_range = (250, 380, 430); + private static readonly DifficultyRange complete_rpm_range = new DifficultyRange(250, 380, 430); public double EndTime { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 25b1dd9b12..0edb8046b9 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -373,10 +373,9 @@ namespace osu.Game.Rulesets.Osu preempt /= rate; adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN); - var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great); - double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, OsuHitWindows.GREAT_WINDOW_RANGE); greatHitWindow /= rate; - adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, OsuHitWindows.GREAT_WINDOW_RANGE); return adjustedDifficulty; } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs index fd86e0eeda..154503c20d 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -1,24 +1,26 @@ // 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 osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { public class OsuHitWindows : HitWindows { + public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(80, 50, 20); + public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(140, 100, 60); + public static readonly DifficultyRange MEH_WINDOW_RANGE = new DifficultyRange(200, 150, 100); + /// /// osu! ruleset has a fixed miss window regardless of difficulty settings. /// public const double MISS_WINDOW = 400; - internal static readonly DifficultyRange[] OSU_RANGES = - { - new DifficultyRange(HitResult.Great, 80, 50, 20), - new DifficultyRange(HitResult.Ok, 140, 100, 60), - new DifficultyRange(HitResult.Meh, 200, 150, 100), - new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW), - }; + private double great; + private double ok; + private double meh; public override bool IsHitResultAllowed(HitResult result) { @@ -34,6 +36,32 @@ namespace osu.Game.Rulesets.Osu.Scoring return false; } - protected override DifficultyRange[] GetRanges() => OSU_RANGES; + public override void SetDifficulty(double difficulty) + { + great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE); + ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE); + meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, MEH_WINDOW_RANGE); + } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Great: + return great; + + case HitResult.Ok: + return ok; + + case HitResult.Meh: + return meh; + + case HitResult.Miss: + return MISS_WINDOW; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs index 6fe61e78b7..7008d8d37a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 6 }); beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 10 }); - var hitWindows = new HitWindows(); + var hitWindows = new DefaultHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); PerformTest(new List diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs index b44ef8ee93..22d268de3b 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs @@ -1,18 +1,21 @@ // 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 osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Scoring { public class TaikoHitWindows : HitWindows { - internal static readonly DifficultyRange[] TAIKO_RANGES = - { - new DifficultyRange(HitResult.Great, 50, 35, 20), - new DifficultyRange(HitResult.Ok, 120, 80, 50), - new DifficultyRange(HitResult.Miss, 135, 95, 70), - }; + public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(50, 35, 20); + public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(120, 80, 50); + public static readonly DifficultyRange MISS_WINDOW_RANGE = new DifficultyRange(135, 95, 70); + + private double great; + private double ok; + private double miss; public override bool IsHitResultAllowed(HitResult result) { @@ -27,6 +30,29 @@ namespace osu.Game.Rulesets.Taiko.Scoring return false; } - protected override DifficultyRange[] GetRanges() => TAIKO_RANGES; + public override void SetDifficulty(double difficulty) + { + great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE); + ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE); + miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, MISS_WINDOW_RANGE); + } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Great: + return great; + + case HitResult.Ok: + return ok; + + case HitResult.Miss: + return miss; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 8cc14ca651..1cb41e1299 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -274,10 +274,9 @@ namespace osu.Game.Rulesets.Taiko { BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); - var greatHitWindowRange = TaikoHitWindows.TAIKO_RANGES.Single(range => range.Result == HitResult.Great); - double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE); greatHitWindow /= rate; - adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max); + adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, TaikoHitWindows.GREAT_WINDOW_RANGE); return adjustedDifficulty; } diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 584a9e09c0..18030d7222 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -359,7 +359,7 @@ namespace osu.Game.Tests.Gameplay } public override Judgement CreateJudgement() => new TestJudgement(maxResult); - protected override HitWindows CreateHitWindows() => new HitWindows(); + protected override HitWindows CreateHitWindows() => new DefaultHitWindows(); private class TestJudgement : Judgement { diff --git a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs index 07d6d68e82..cfe523fdd5 100644 --- a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs +++ b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.NonVisual public void TestResultIfOnlyParentHitWindowIsEmpty() { var testObject = new TestHitObject(HitWindows.Empty); - HitObject nested = new TestHitObject(new HitWindows()); + HitObject nested = new TestHitObject(new DefaultHitWindows()); testObject.AddNested(nested); testDrawableRuleset.HitObjects = new List { testObject }; @@ -43,8 +43,8 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestResultIfParentHitWindowsIsNotEmpty() { - var testObject = new TestHitObject(new HitWindows()); - HitObject nested = new TestHitObject(new HitWindows()); + var testObject = new TestHitObject(new DefaultHitWindows()); + HitObject nested = new TestHitObject(new DefaultHitWindows()); testObject.AddNested(nested); testDrawableRuleset.HitObjects = new List { testObject }; @@ -58,7 +58,7 @@ namespace osu.Game.Tests.NonVisual HitObject nested = new TestHitObject(HitWindows.Empty); firstObject.AddNested(nested); - var secondObject = new TestHitObject(new HitWindows()); + var secondObject = new TestHitObject(new DefaultHitWindows()); testDrawableRuleset.HitObjects = new List { firstObject, secondObject }; Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, secondObject.HitWindows); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 6981591193..894b51ddcb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -65,30 +65,30 @@ namespace osu.Game.Tests.Visual.Gameplay { new HitCircle { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } }, new HitCircle { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) } }, new HitCircle { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) }, }, new HitCircle { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, }, new Slider { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), StartTime = t += spacing, Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }), Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) }, diff --git a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs index 48f6564084..2dd73a2541 100644 --- a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs @@ -92,8 +92,8 @@ namespace osu.Game.Beatmaps /// /// /// Value to which the difficulty value maps in the specified range. - static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) - => DifficultyRange(difficulty, range.od0, range.od5, range.od10); + static double DifficultyRange(double difficulty, DifficultyRange range) + => DifficultyRange(difficulty, range.Min, range.Mid, range.Max); /// /// Inverse function to . @@ -110,5 +110,23 @@ namespace osu.Game.Beatmaps ? (difficultyValue - diff5) / (diff10 - diff5) * 5 + 5 : (difficultyValue - diff5) / (diff5 - diff0) * 5 + 5; } + + /// + /// Inverse function to . + /// Maps a value returned by the function above back to the difficulty that produced it. + /// + /// The difficulty-dependent value to be unmapped. + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Value to which the difficulty value maps in the specified range. + static double InverseDifficultyRange(double difficultyValue, DifficultyRange range) + => InverseDifficultyRange(difficultyValue, range.Min, range.Mid, range.Max); } + + /// + /// Represents a piecewise-linear difficulty curve for a given gameplay quantity. + /// + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + public record struct DifficultyRange(double Min, double Mid, double Max); } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 07e07b25d3..61c6c9f46f 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Objects /// /// [NotNull] - protected virtual HitWindows CreateHitWindows() => new HitWindows(); + protected virtual HitWindows CreateHitWindows() => new DefaultHitWindows(); /// /// The maximum offset from the end time of at which this can be judged. diff --git a/osu.Game/Rulesets/Scoring/DefaultHitWindows.cs b/osu.Game/Rulesets/Scoring/DefaultHitWindows.cs new file mode 100644 index 0000000000..3048233335 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/DefaultHitWindows.cs @@ -0,0 +1,66 @@ +// 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 osu.Game.Beatmaps; + +namespace osu.Game.Rulesets.Scoring +{ + /// + /// An example implementation of . + /// Not meaningfully used, provided mostly as a reference to ruleset implementors. + /// + public class DefaultHitWindows : HitWindows + { + private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D); + private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34); + private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67); + private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97); + private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121); + private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158); + + private double perfect; + private double great; + private double good; + private double ok; + private double meh; + private double miss; + + public override void SetDifficulty(double difficulty) + { + perfect = IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range); + great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range); + good = IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range); + ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range); + meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range); + miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range); + } + + public override double WindowFor(HitResult result) + { + switch (result) + { + case HitResult.Perfect: + return perfect; + + case HitResult.Great: + return great; + + case HitResult.Good: + return good; + + case HitResult.Ok: + return ok; + + case HitResult.Meh: + return meh; + + case HitResult.Miss: + return miss; + + default: + throw new ArgumentOutOfRangeException(nameof(result), result, null); + } + } + } +} diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 94ea51c0b2..e1429f32b2 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring @@ -13,35 +12,19 @@ namespace osu.Game.Rulesets.Scoring /// /// A structure containing timing data for hit window based gameplay. /// - public class HitWindows + public abstract class HitWindows { - private static readonly DifficultyRange[] base_ranges = - { - new DifficultyRange(HitResult.Perfect, 22.4D, 19.4D, 13.9D), - new DifficultyRange(HitResult.Great, 64, 49, 34), - new DifficultyRange(HitResult.Good, 97, 82, 67), - new DifficultyRange(HitResult.Ok, 127, 112, 97), - new DifficultyRange(HitResult.Meh, 151, 136, 121), - new DifficultyRange(HitResult.Miss, 188, 173, 158), - }; - - private double perfect; - private double great; - private double good; - private double ok; - private double meh; - private double miss; - /// /// An empty with only and . /// No time values are provided (meaning instantaneous hit or miss). /// public static HitWindows Empty { get; } = new EmptyHitWindows(); - public HitWindows() + protected HitWindows() { - Debug.Assert(GetRanges().Any(r => r.Result == HitResult.Miss), $"{nameof(GetRanges)} should always contain {nameof(HitResult.Miss)}"); - Debug.Assert(GetRanges().Any(r => r.Result != HitResult.Miss), $"{nameof(GetRanges)} should always contain at least one result type other than {nameof(HitResult.Miss)}."); + var availableWindows = GetAllAvailableWindows(); + Debug.Assert(availableWindows.Any(r => r.result == HitResult.Miss), $"{nameof(GetAllAvailableWindows)} should always contain {nameof(HitResult.Miss)}"); + Debug.Assert(availableWindows.Any(r => r.result != HitResult.Miss), $"{nameof(GetAllAvailableWindows)} should always contain at least one result type other than {nameof(HitResult.Miss)}."); } /// @@ -64,7 +47,7 @@ namespace osu.Game.Rulesets.Scoring /// public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows() { - for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) + for (var result = HitResult.Miss; result <= HitResult.Perfect; ++result) { if (IsHitResultAllowed(result)) yield return (result, WindowFor(result)); @@ -82,40 +65,7 @@ namespace osu.Game.Rulesets.Scoring /// Sets hit windows with values that correspond to a difficulty parameter. /// /// The parameter. - public void SetDifficulty(double difficulty) - { - foreach (var range in GetRanges()) - { - double value = IBeatmapDifficultyInfo.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); - - switch (range.Result) - { - case HitResult.Miss: - miss = value; - break; - - case HitResult.Meh: - meh = value; - break; - - case HitResult.Ok: - ok = value; - break; - - case HitResult.Good: - good = value; - break; - - case HitResult.Great: - great = value; - break; - - case HitResult.Perfect: - perfect = value; - break; - } - } - } + public abstract void SetDifficulty(double difficulty); /// /// Retrieves the for a time offset. @@ -141,35 +91,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The expected . /// One half of the hit window for . - public double WindowFor(HitResult result) - { - if (!IsHitResultAllowed(result)) - throw new ArgumentOutOfRangeException(nameof(result), result, $@"{result} is not an allowed result."); - - switch (result) - { - case HitResult.Perfect: - return perfect; - - case HitResult.Great: - return great; - - case HitResult.Good: - return good; - - case HitResult.Ok: - return ok; - - case HitResult.Meh: - return meh; - - case HitResult.Miss: - return miss; - - default: - throw new ArgumentOutOfRangeException(nameof(result), result, null); - } - } + public abstract double WindowFor(HitResult result); /// /// Given a time offset, whether the can ever be hit in the future with a non- result. @@ -179,41 +101,13 @@ namespace osu.Game.Rulesets.Scoring /// Whether the can be hit at any point in the future from this time offset. public bool CanBeHit(double timeOffset) => timeOffset <= WindowFor(LowestSuccessfulHitResult()); - /// - /// Retrieve a valid list of s representing hit windows. - /// Defaults are provided but can be overridden to customise for a ruleset. - /// - protected virtual DifficultyRange[] GetRanges() => base_ranges; - private class EmptyHitWindows : HitWindows { - private static readonly DifficultyRange[] ranges = - { - new DifficultyRange(HitResult.Perfect, 0, 0, 0), - new DifficultyRange(HitResult.Miss, 0, 0, 0), - }; - public override bool IsHitResultAllowed(HitResult result) => true; - protected override DifficultyRange[] GetRanges() => ranges; - } - } + public override void SetDifficulty(double difficulty) { } - public struct DifficultyRange - { - public readonly HitResult Result; - - public double Min; - public double Average; - public double Max; - - public DifficultyRange(HitResult result, double min, double average, double max) - { - Result = result; - - Min = min; - Average = average; - Max = max; + public override double WindowFor(HitResult result) => 0; } } } diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index a71a46ec2a..e27a7544c9 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters const int bar_width = 2; const float chevron_size = 8; - hitWindows = HitWindows.GetAllAvailableWindows().ToArray(); + hitWindows = HitWindows.GetAllAvailableWindows().Where(w => w.result.IsHit()).ToArray(); InternalChild = new Container { diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs index 0f328d04fb..c5c4d7d5b2 100644 --- a/osu.Game/Screens/Utility/CircleGameplay.cs +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Utility HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), }, null, null); Hit?.Invoke(HitEvent.Value); diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs index c0264f5734..3e0969b625 100644 --- a/osu.Game/Screens/Utility/ScrollingGameplay.cs +++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Utility HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { - HitWindows = new HitWindows(), + HitWindows = new DefaultHitWindows(), }, null, null); Hit?.Invoke(HitEvent.Value);