// 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; using osu.Game.Rulesets.Objects; 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 UnstableRateCalculationResult? CalculateUnstableRate(this IReadOnlyList hitEvents, UnstableRateCalculationResult? result = null) { Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); result ??= new UnstableRateCalculationResult(); // Handle rewinding in the simplest way possible. if (hitEvents.Count < result.EventCount + 1) result = new UnstableRateCalculationResult(); for (int i = result.EventCount; i < hitEvents.Count; i++) { HitEvent e = hitEvents[i]; if (!AffectsUnstableRate(e)) continue; result.EventCount++; // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. double currentValue = e.TimeOffset / e.GameplayRate!.Value; double nextMean = result.Mean + (currentValue - result.Mean) / result.EventCount; result.SumOfSquares += (currentValue - result.Mean) * (currentValue - nextMean); result.Mean = nextMean; } if (result.EventCount == 0) return null; 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(); } 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(); /// /// Data type returned by which allows efficient incremental processing. /// /// /// This should be passed back into future calls as a parameter. /// /// The optimisations used here rely on hit events being a consecutive sequence from a single gameplay session. /// When a new gameplay session is started, any existing results should be disposed. /// public class UnstableRateCalculationResult { /// /// Total events processed. For internal incremental calculation use. /// public int EventCount; /// /// Last sum-of-squares value. For internal incremental calculation use. /// public double SumOfSquares; /// /// Last mean value. For internal incremental calculation use. /// public double Mean; /// /// The unstable rate. /// public double Result => EventCount == 0 ? 0 : 10.0 * Math.Sqrt(SumOfSquares / EventCount); } } }