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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
use std::cmp;
use crate::util::{float_ext::FloatExt, hint::unlikely};
/// osu!standard hitresults.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OsuHitResults {
/// "Large tick" hits.
///
/// The meaning depends on the kind of score:
/// - if set on osu!stable, this field is irrelevant and can be `0`
/// - if set on osu!lazer *with* slider accuracy, this field is the amount
/// of hit slider ticks and repeats
/// - if set on osu!lazer *without* slider accuracy, this field is the
/// amount of hit slider heads, ticks, and repeats
///
/// Only relevant for osu!lazer.
pub large_tick_hits: u32,
/// "Small ticks" hits.
///
/// These are essentially the slider end hits for lazer scores without
/// slider accuracy.
///
/// Only relevant for osu!lazer.
pub small_tick_hits: u32,
/// Amount of successfully hit slider ends.
///
/// Only relevant for osu!lazer.
pub slider_end_hits: u32,
/// Amount of 300s.
pub n300: u32,
/// Amount of 100s.
pub n100: u32,
/// Amount of 50s.
pub n50: u32,
/// Amount of misses.
pub misses: u32,
}
impl OsuHitResults {
/// Create new empty hitresults.
pub const fn new() -> Self {
Self {
large_tick_hits: 0,
small_tick_hits: 0,
slider_end_hits: 0,
n300: 0,
n100: 0,
n50: 0,
misses: 0,
}
}
/// Return the total amount of hits by adding everything up.
pub const fn total_hits(&self) -> u32 {
self.n300 + self.n100 + self.n50 + self.misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
pub fn accuracy(&self, origin: OsuScoreOrigin) -> f64 {
let mut numerator = f64::from(6 * self.n300 + 2 * self.n100 + self.n50);
let mut denominator = f64::from(6 * (self.n300 + self.n100 + self.n50 + self.misses));
match origin {
OsuScoreOrigin::Stable => {}
OsuScoreOrigin::WithSliderAcc {
max_large_ticks,
max_slider_ends,
} => {
let slider_end_hits = cmp::min(self.slider_end_hits, max_slider_ends);
let large_tick_hits = cmp::min(self.large_tick_hits, max_large_ticks);
numerator += f64::from(3 * slider_end_hits) + 0.6 * f64::from(large_tick_hits);
denominator += f64::from(3 * max_slider_ends) + 0.6 * f64::from(max_large_ticks);
}
OsuScoreOrigin::WithoutSliderAcc {
max_large_ticks,
max_small_ticks,
} => {
let large_tick_hits = cmp::min(self.large_tick_hits, max_large_ticks);
let small_tick_hits = cmp::min(self.small_tick_hits, max_small_ticks);
numerator += 0.6 * f64::from(large_tick_hits) + 0.2 * f64::from(small_tick_hits);
denominator += 0.6 * f64::from(max_large_ticks) + 0.2 * f64::from(max_small_ticks);
}
}
if unlikely(denominator.eq(0.0)) {
0.0
} else {
numerator / denominator
}
}
}
impl Default for OsuHitResults {
fn default() -> Self {
Self::new()
}
}
/// Aggregation for a score's current state.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OsuScoreState {
/// Maximum combo that the score has had so far. **Not** the maximum
/// possible combo of the map so far.
pub max_combo: u32,
/// Hitresults of a score.
pub hitresults: OsuHitResults,
/// Legacy total score.
///
/// Only relevant for osu!stable
pub legacy_total_score: Option<u32>,
}
impl OsuScoreState {
/// Create a new empty score state.
pub const fn new() -> Self {
Self {
max_combo: 0,
hitresults: OsuHitResults::new(),
legacy_total_score: None,
}
}
}
impl Default for OsuScoreState {
fn default() -> Self {
Self::new()
}
}
/// Type to pass [`OsuHitResults::accuracy`] and specify the origin of a score.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum OsuScoreOrigin {
/// For scores set on osu!stable
Stable,
/// For scores set on osu!lazer with slider accuracy
WithSliderAcc {
max_large_ticks: u32,
max_slider_ends: u32,
},
/// For scores set on osu!lazer without slider accuracy
WithoutSliderAcc {
max_large_ticks: u32,
max_small_ticks: u32,
},
}
impl OsuScoreOrigin {
/// Returns the tick score and tick max for this origin based on the
/// accuracy formula:
///
/// ```ignore
/// acc = (300*n300 + 100*n100 + 50*n50 + tick_score) / (300*total_hits + tick_max)
/// // => returns (tick_score, tick_max)
/// ```
pub(crate) fn tick_scores(
&self,
large_tick_hits: u32,
small_tick_hits: u32,
slider_end_hits: u32,
) -> (u32, u32) {
match self {
Self::Stable => (0, 0),
Self::WithSliderAcc {
max_large_ticks,
max_slider_ends,
} => (
150 * slider_end_hits + 30 * large_tick_hits,
150 * max_slider_ends + 30 * max_large_ticks,
),
Self::WithoutSliderAcc {
max_large_ticks,
max_small_ticks,
} => (
30 * large_tick_hits + 10 * small_tick_hits,
30 * max_large_ticks + 10 * max_small_ticks,
),
}
}
}