// 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 System.Diagnostics; using System.Linq; namespace osu.Game.Rulesets.Scoring { public static class HitEventExtensions { /// /// Calculates the "unstable rate" for a sequence of s. /// /// /// Uses Welford's online algorithm. /// /// /// A non-null value if unstable rate could be calculated, /// and if unstable rate cannot be calculated due to being empty. /// public static double? CalculateUnstableRate(this IEnumerable hitEvents) { Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); int count = 0; double mean = 0; double sumOfSquares = 0; foreach (var e in hitEvents) { if (!affectsUnstableRate(e)) continue; count++; // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. double currentValue = e.TimeOffset / e.GameplayRate!.Value; double nextMean = mean + (currentValue - mean) / count; sumOfSquares += (currentValue - mean) * (currentValue - nextMean); mean = nextMean; } if (count == 0) return null; return 10.0 * Math.Sqrt(sumOfSquares / count); } /// /// 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(); } private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); } }