diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 180b9ef71b..859b6cfe76 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -394,6 +394,7 @@ namespace osu.Game.Rulesets.Mania
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
+ new AverageHitError(score.HitEvents),
new UnstableRate(score.HitEvents)
}), true)
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index ad00a025a1..5ade164566 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -314,6 +314,7 @@ namespace osu.Game.Rulesets.Osu
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
+ new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index e56aabaf9d..de0ef8d95b 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -237,6 +237,7 @@ namespace osu.Game.Rulesets.Taiko
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
+ new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
}
diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
index f645b12483..637d0a872a 100644
--- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
+++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs
@@ -22,6 +22,16 @@ namespace osu.Game.Rulesets.Scoring
return 10 * standardDeviation(timeOffsets);
}
+ ///
+ /// 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) =>
+ hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).Average();
+
private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit();
private static double? standardDeviation(double[] timeOffsets)
diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs
new file mode 100644
index 0000000000..d0e70251e7
--- /dev/null
+++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs
@@ -0,0 +1,27 @@
+// 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 osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Screens.Ranking.Statistics
+{
+ ///
+ /// Displays the unstable rate statistic for a given play.
+ ///
+ public class AverageHitError : SimpleStatisticItem
+ {
+ ///
+ /// Creates and computes an statistic.
+ ///
+ /// Sequence of s to calculate the unstable rate based on.
+ public AverageHitError(IEnumerable hitEvents)
+ : base("Average Hit Error")
+ {
+ Value = hitEvents.CalculateAverageHitError();
+ }
+
+ protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}";
+ }
+}
diff --git a/osu.iOS.props b/osu.iOS.props
index 9d0e1790f0..80600655aa 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -15,7 +15,8 @@
--nosymbolstrip=BASS_FX_BPM_BeatCallbackReset --nosymbolstrip=BASS_FX_BPM_BeatCallbackSet --nosymbolstrip=BASS_FX_BPM_BeatDecodeGet --nosymbolstrip=BASS_FX_BPM_BeatFree --nosymbolstrip=BASS_FX_BPM_BeatGetParameters --nosymbolstrip=BASS_FX_BPM_BeatSetParameters --nosymbolstrip=BASS_FX_BPM_CallbackReset --nosymbolstrip=BASS_FX_BPM_CallbackSet --nosymbolstrip=BASS_FX_BPM_DecodeGet --nosymbolstrip=BASS_FX_BPM_Free --nosymbolstrip=BASS_FX_BPM_Translate --nosymbolstrip=BASS_FX_GetVersion --nosymbolstrip=BASS_FX_ReverseCreate --nosymbolstrip=BASS_FX_ReverseGetSource --nosymbolstrip=BASS_FX_TempoCreate --nosymbolstrip=BASS_FX_TempoGetRateRatio --nosymbolstrip=BASS_FX_TempoGetSource --nosymbolstrip=BASS_Mixer_ChannelFlags --nosymbolstrip=BASS_Mixer_ChannelGetData --nosymbolstrip=BASS_Mixer_ChannelGetEnvelopePos --nosymbolstrip=BASS_Mixer_ChannelGetLevel --nosymbolstrip=BASS_Mixer_ChannelGetLevelEx --nosymbolstrip=BASS_Mixer_ChannelGetMatrix --nosymbolstrip=BASS_Mixer_ChannelGetMixer --nosymbolstrip=BASS_Mixer_ChannelGetPosition --nosymbolstrip=BASS_Mixer_ChannelGetPositionEx --nosymbolstrip=BASS_Mixer_ChannelIsActive --nosymbolstrip=BASS_Mixer_ChannelRemove --nosymbolstrip=BASS_Mixer_ChannelRemoveSync --nosymbolstrip=BASS_Mixer_ChannelSetEnvelope --nosymbolstrip=BASS_Mixer_ChannelSetEnvelopePos --nosymbolstrip=BASS_Mixer_ChannelSetMatrix --nosymbolstrip=BASS_Mixer_ChannelSetMatrixEx --nosymbolstrip=BASS_Mixer_ChannelSetPosition --nosymbolstrip=BASS_Mixer_ChannelSetSync --nosymbolstrip=BASS_Mixer_GetVersion --nosymbolstrip=BASS_Mixer_StreamAddChannel --nosymbolstrip=BASS_Mixer_StreamAddChannelEx --nosymbolstrip=BASS_Mixer_StreamCreate --nosymbolstrip=BASS_Mixer_StreamGetChannels --nosymbolstrip=BASS_Split_StreamCreate --nosymbolstrip=BASS_Split_StreamGetAvailable --nosymbolstrip=BASS_Split_StreamGetSource --nosymbolstrip=BASS_Split_StreamGetSplits --nosymbolstrip=BASS_Split_StreamReset --nosymbolstrip=BASS_Split_StreamResetEx
- --nolinkaway $(GeneratedMtouchSymbolStripFlags)
+
+ --nolinkaway --nostrip $(GeneratedMtouchSymbolStripFlags)
true