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