Skip to content

Refactor hit windows class structure to reduce rigidity #33875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 25, 2025

Conversation

bdach
Copy link
Collaborator

@bdach bdach commented Jun 25, 2025

Split out from #33078

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 only 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.

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.
@bdach bdach requested review from peppy and smoogipoo June 25, 2025 06:46
@bdach bdach self-assigned this Jun 25, 2025
{
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might seem pedantic, but I'd want to avoid this call here in release builds since it is being constructed at least once per hitobject.

diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs
index e1429f32b2..9463fd391b 100644
--- a/osu.Game/Rulesets/Scoring/HitWindows.cs
+++ b/osu.Game/Rulesets/Scoring/HitWindows.cs
@@ -21,10 +21,17 @@ public abstract class HitWindows
         public static HitWindows Empty { get; } = new EmptyHitWindows();
 
         protected HitWindows()
+        {
+            ensureValidHitWindows();
+        }
+
+        [Conditional("DEBUG")]
+        private void ensureValidHitWindows()
         {
             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)}.");
+            Debug.Assert(availableWindows.Any(r => r.result != HitResult.Miss),
+                $"{nameof(GetAllAvailableWindows)} should always contain at least one result type other than {nameof(HitResult.Miss)}.");
         }
 
         /// <summary>

maybe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, applied (with an extra .ToList() to shut a multiple enumeration inspection up) in 55ff5a7.

@smoogipoo smoogipoo merged commit 09e31b7 into ppy:master Jun 25, 2025
9 checks passed
@bdach bdach deleted the hitwindow-refactor branch June 25, 2025 09:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants