From bb8f8e8d8c709069d41338afb01206869dee4c99 Mon Sep 17 00:00:00 2001
From: sineplusx <112003827+sineplusx@users.noreply.github.com>
Date: Wed, 26 Mar 2025 15:41:48 +0100
Subject: [PATCH 1/8] Use median instead of mean for automatic beatmap offset
adjustment
---
.../Rulesets/Scoring/HitEventExtensions.cs | 19 +++++++++++++++++++
.../PlayerSettings/BeatmapOffsetControl.cs | 4 ++--
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
index fed0c3b51b..da1ac9f2a1 100644
--- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
+++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
@@ -71,6 +71,25 @@ namespace osu.Game.Rulesets.Scoring
return timeOffsets.Average();
}
+ ///
+ /// Calculates the median hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average.
+ ///
+ ///
+ /// A non-null value if unstable rate could be calculated,
+ /// and if unstable rate cannot be calculated due to being empty.
+ ///
+ public static double? CalculateMedianHitError(this IEnumerable hitEvents)
+ {
+ double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).OrderBy(x => x).ToArray();
+
+ if (timeOffsets.Length == 0)
+ return null;
+
+ int center = timeOffsets.Length / 2;
+
+ return timeOffsets.Length % 2 == 0 ? (timeOffsets[center - 1] + timeOffsets[center]) / 2 : timeOffsets[center];
+ }
+
public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result);
public static bool AffectsUnstableRate(HitObject hitObject, HitResult result) => hitObject.HitWindows != HitWindows.Empty && result.IsHit();
diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
index cef5884d39..ce474ed594 100644
--- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
var hitEvents = score.NewValue.HitEvents;
- if (!(hitEvents.CalculateAverageHitError() is double average))
+ if (!(hitEvents.CalculateMedianHitError() is double median))
return;
referenceScoreContainer.Children = new Drawable[]
@@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
return;
}
- lastPlayAverage = average;
+ lastPlayAverage = median;
lastPlayBeatmapOffset = Current.Value;
LinkFlowContainer globalOffsetText;
From 6452514066161dd6c38e533c5564f6c0249abb59 Mon Sep 17 00:00:00 2001
From: sineplusx <112003827+sineplusx@users.noreply.github.com>
Date: Wed, 26 Mar 2025 17:22:57 +0100
Subject: [PATCH 2/8] Add comment
---
osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
index da1ac9f2a1..01d800a351 100644
--- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
+++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
@@ -87,6 +87,7 @@ namespace osu.Game.Rulesets.Scoring
int center = timeOffsets.Length / 2;
+ // Use average of the 2 central values if length is even
return timeOffsets.Length % 2 == 0 ? (timeOffsets[center - 1] + timeOffsets[center]) / 2 : timeOffsets[center];
}
From 77d73c5f50ef2a36877f0a1b33e0cdab356ead98 Mon Sep 17 00:00:00 2001
From: sineplusx <112003827+sineplusx@users.noreply.github.com>
Date: Wed, 26 Mar 2025 17:23:19 +0100
Subject: [PATCH 3/8] Increase number of timed hits needed to activate button
---
osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
index ce474ed594..c2cd09c56f 100644
--- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
@@ -210,7 +210,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
// affecting unstable rate here is used as a substitute of determining if a hit event represents a *timed* hit event,
// i.e. an user input that the user had to *time to the track*,
// i.e. one that it *makes sense to use* when doing anything with timing and offsets.
- if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 10)
+ if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 50)
{
referenceScoreContainer.AddRange(new Drawable[]
{
From 4b8fe015e56614b1a98c3e2081ff9606d0d3bd9b Mon Sep 17 00:00:00 2001
From: sineplusx <112003827+sineplusx@users.noreply.github.com>
Date: Wed, 26 Mar 2025 17:53:03 +0100
Subject: [PATCH 4/8] Apply median to `SessionAverageHitErrorTracker`
---
osu.Game/Configuration/SessionAverageHitErrorTracker.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/osu.Game/Configuration/SessionAverageHitErrorTracker.cs b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs
index cd21eb6fa8..49f7657f91 100644
--- a/osu.Game/Configuration/SessionAverageHitErrorTracker.cs
+++ b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs
@@ -40,10 +40,10 @@ namespace osu.Game.Configuration
if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs))
return;
- if (newScore.HitEvents.Count < 10)
+ if (newScore.HitEvents.Count < 50)
return;
- if (newScore.HitEvents.CalculateAverageHitError() is not double averageError)
+ if (newScore.HitEvents.CalculateMedianHitError() is not double medianError)
return;
// keep a sane maximum number of entries.
@@ -51,7 +51,7 @@ namespace osu.Game.Configuration
averageHitErrorHistory.RemoveAt(0);
double globalOffset = configManager.Get(OsuSetting.AudioOffset);
- averageHitErrorHistory.Add(new DataPoint(averageError, globalOffset));
+ averageHitErrorHistory.Add(new DataPoint(medianError, globalOffset));
}
public void ClearHistory() => averageHitErrorHistory.Clear();
From fa06643bb6c0aacde659640ae0a65c68ab9b0c61 Mon Sep 17 00:00:00 2001
From: sineplusx <112003827+sineplusx@users.noreply.github.com>
Date: Sat, 29 Mar 2025 13:44:14 +0100
Subject: [PATCH 5/8] Use median for statistic display
---
osu.Game/Screens/Ranking/Statistics/AverageHitError.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs
index fb7107cc88..29df085c62 100644
--- a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs
+++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs
@@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Ranking.Statistics
{
///
- /// Displays the unstable rate statistic for a given play.
+ /// Displays the average hit error statistic for a given play.
///
public partial class AverageHitError : SimpleStatisticItem
{
///
/// Creates and computes an statistic.
///
- /// Sequence of s to calculate the unstable rate based on.
+ /// Sequence of s to calculate the average hit error based on.
public AverageHitError(IEnumerable hitEvents)
: base("Average Hit Error")
{
- Value = hitEvents.CalculateAverageHitError();
+ Value = hitEvents.CalculateMedianHitError();
}
protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}";
From b3c578e5455c572e34e2def301ba657182747149 Mon Sep 17 00:00:00 2001
From: sineplusx <112003827+sineplusx@users.noreply.github.com>
Date: Sat, 29 Mar 2025 13:45:39 +0100
Subject: [PATCH 6/8] Remove mean hit error calculation
---
osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 17 -----------------
1 file changed, 17 deletions(-)
diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
index 01d800a351..39fc8b357b 100644
--- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
+++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
@@ -54,23 +54,6 @@ namespace osu.Game.Rulesets.Scoring
return result;
}
- ///
- /// Calculates the average hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average.
- ///
- ///
- /// A non-null value if unstable rate could be calculated,
- /// and if unstable rate cannot be calculated due to being empty.
- ///
- public static double? CalculateAverageHitError(this IEnumerable hitEvents)
- {
- double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
-
- if (timeOffsets.Length == 0)
- return null;
-
- return timeOffsets.Average();
- }
-
///
/// Calculates the median hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average.
///
From 403b24ef9bb32d95dc3f701ee9fce8b217d16848 Mon Sep 17 00:00:00 2001
From: sineplusx <112003827+sineplusx@users.noreply.github.com>
Date: Sat, 29 Mar 2025 20:43:59 +0100
Subject: [PATCH 7/8] Adapt `TestNotEnoughTimedHitEvents` with new minimum hit
amount
---
.../Gameplay/TestSceneBeatmapOffsetControl.cs | 16 ++++++----------
1 file changed, 6 insertions(+), 10 deletions(-)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
index aa99b22701..92a10628ff 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
@@ -50,21 +50,17 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("Set short reference score", () =>
{
+ // 50 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
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),
];
+ for (int i = 0; i < 49; i++)
+ {
+ hitEvents.Add(new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null));
+ }
+
foreach (var ev in hitEvents)
ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
From 332b08160388cc0eaef2d22f5197ce9e5ee910ba Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Thu, 3 Apr 2025 18:55:45 +0900
Subject: [PATCH 8/8] Rename some variables
---
.../Play/PlayerSettings/BeatmapOffsetControl.cs | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
index c2cd09c56f..23ccb3311b 100644
--- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
@@ -63,11 +63,11 @@ namespace osu.Game.Screens.Play.PlayerSettings
[Resolved]
private IGameplayClock? gameplayClock { get; set; }
- private double lastPlayAverage;
+ private double lastPlayMedian;
private double lastPlayBeatmapOffset;
private HitEventTimingDistributionGraph? lastPlayGraph;
- private SettingsButton? useAverageButton;
+ private SettingsButton? calibrateFromLastPlayButton;
private IDisposable? beatmapOffsetSubscription;
@@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
return;
}
- lastPlayAverage = median;
+ lastPlayMedian = median;
lastPlayBeatmapOffset = Current.Value;
LinkFlowContainer globalOffsetText;
@@ -239,7 +239,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
Height = 50,
},
new AverageHitError(hitEvents),
- useAverageButton = new SettingsButton
+ calibrateFromLastPlayButton = new SettingsButton
{
Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay,
Action = () =>
@@ -247,7 +247,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
if (Current.Disabled)
return;
- Current.Value = lastPlayBeatmapOffset - lastPlayAverage;
+ Current.Value = lastPlayBeatmapOffset - lastPlayMedian;
lastAppliedScore.Value = ReferenceScore.Value;
},
},
@@ -281,8 +281,8 @@ namespace osu.Game.Screens.Play.PlayerSettings
bool allow = allowOffsetAdjust;
- if (useAverageButton != null)
- useAverageButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2);
+ if (calibrateFromLastPlayButton != null)
+ calibrateFromLastPlayButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayMedian, adjustmentSinceLastPlay, Current.Precision / 2);
Current.Disabled = !allow;
}