// 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(); public class UnstableRateCalculationResult { public int EventCount; public double SumOfSquares; public double Mean; public double Result => 10.0 * Math.Sqrt(SumOfSquares / EventCount); } } }