forked from ppy/osu
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHitEventExtensions.cs
More file actions
145 lines (120 loc) · 6 KB
/
Copy pathHitEventExtensions.cs
File metadata and controls
145 lines (120 loc) · 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// Calculates the "unstable rate" for a sequence of <see cref="HitEvent"/>s.
/// </summary>
/// <remarks>
/// Uses <a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm">Welford's online algorithm</a>.
/// </remarks>
/// <returns>
/// A non-null <see langword="double"/> value if unstable rate could be calculated,
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
/// </returns>
public static UnstableRateCalculationResult? CalculateUnstableRate(this IReadOnlyList<HitEvent> 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.LastProcessedIndex + 1)
result = new UnstableRateCalculationResult();
for (int i = result.LastProcessedIndex + 1; i < hitEvents.Count; i++)
{
result.LastProcessedIndex = 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;
}
/// <summary>
/// Calculates the average hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
/// </summary>
/// <returns>
/// A non-null <see langword="double"/> value if unstable rate could be calculated,
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
/// </returns>
public static double? CalculateAverageHitError(this IEnumerable<HitEvent> hitEvents)
{
// Single-pass mean using running sum — avoids allocating a temporary array.
double sum = 0;
int count = 0;
foreach (var ev in hitEvents)
{
if (!AffectsUnstableRate(ev))
continue;
sum += ev.TimeOffset;
count++;
}
if (count == 0)
return null;
return sum / count;
}
/// <summary>
/// Calculates the median hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
/// </summary>
/// <returns>
/// A non-null <see langword="double"/> value if unstable rate could be calculated,
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
/// </returns>
public static double? CalculateMedianHitError(this IEnumerable<HitEvent> hitEvents)
{
double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).OrderBy(x => x).ToArray();
if (timeOffsets.Length == 0)
return null;
int center = timeOffsets.Length / 2;
// Use average of the 2 central values if length is even
return timeOffsets.Length % 2 == 0 ? (timeOffsets[center - 1] + timeOffsets[center]) / 2 : timeOffsets[center];
}
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();
/// <summary>
/// Data type returned by <see cref="HitEventExtensions.CalculateUnstableRate"/> which allows efficient incremental processing.
/// </summary>
/// <remarks>
/// This should be passed back into future <see cref="HitEventExtensions.CalculateUnstableRate"/> 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.
/// </remarks>
public class UnstableRateCalculationResult
{
/// <summary>
/// The last result index processed. For internal incremental calculation use.
/// </summary>
public int LastProcessedIndex = -1;
/// <summary>
/// Total events processed. For internal incremental calculation use.
/// </summary>
public int EventCount;
/// <summary>
/// Last sum-of-squares value. For internal incremental calculation use.
/// </summary>
public double SumOfSquares;
/// <summary>
/// Last mean value. For internal incremental calculation use.
/// </summary>
public double Mean;
/// <summary>
/// The unstable rate.
/// </summary>
public double Result => EventCount == 0 ? 0 : 10.0 * Math.Sqrt(SumOfSquares / EventCount);
}
}
}