rosu_pp/mania/performance/gradual.rs
1use crate::{mania::ManiaGradualDifficulty, model::mode::ConvertError, Beatmap, Difficulty};
2
3use super::{ManiaPerformanceAttributes, ManiaScoreState};
4
5/// Gradually calculate the performance attributes of an osu!mania map.
6///
7/// After each hit object you can call [`next`] and it will return the
8/// resulting current [`ManiaPerformanceAttributes`]. To process multiple
9/// objects at once, use [`nth`] instead.
10///
11/// Both methods require a play's current score so far. Be sure the given score
12/// is adjusted with respect to mods.
13///
14/// If you only want to calculate difficulty attributes use
15/// [`ManiaGradualDifficulty`] instead.
16///
17/// # Example
18///
19/// ```
20/// use rosu_pp::{Beatmap, Difficulty};
21/// use rosu_pp::mania::{Mania, ManiaGradualPerformance, ManiaScoreState};
22///
23/// let map = Beatmap::from_path("./resources/1638954.osu").unwrap();
24///
25/// let difficulty = Difficulty::new().mods(64); // DT
26/// let mut gradual = ManiaGradualPerformance::new(difficulty, &map).unwrap();
27/// let mut state = ManiaScoreState::new(); // empty state, everything is on 0.
28///
29/// // The first 10 hitresults are 320s
30/// for _ in 0..10 {
31/// state.n320 += 1;
32///
33/// let attrs = gradual.next(state.clone()).unwrap();
34/// println!("PP: {}", attrs.pp);
35/// }
36///
37/// // Then comes a miss.
38/// state.misses += 1;
39/// let attrs = gradual.next(state.clone()).unwrap();
40/// println!("PP: {}", attrs.pp);
41///
42/// // The next 10 objects will be a mixture of 320s and 100s.
43/// // Notice how all 10 objects will be processed in one go.
44/// state.n320 += 3;
45/// state.n100 += 7;
46/// // The `nth` method takes a zero-based value.
47/// let attrs = gradual.nth(state.clone(), 9).unwrap();
48/// println!("PP: {}", attrs.pp);
49///
50/// // Skip to the end
51/// # /*
52/// state.max_combo = ...
53/// state.n300 = ...
54/// state.n100 = ...
55/// state.misses = ...
56/// # */
57/// let attrs = gradual.last(state.clone()).unwrap();
58/// println!("PP: {}", attrs.pp);
59///
60/// // Once the final performance was calculated,
61/// // attempting to process further objects will return `None`.
62/// assert!(gradual.next(state).is_none());
63/// ```
64///
65/// [`next`]: ManiaGradualPerformance::next
66/// [`nth`]: ManiaGradualPerformance::nth
67pub struct ManiaGradualPerformance {
68 difficulty: ManiaGradualDifficulty,
69}
70
71impl ManiaGradualPerformance {
72 /// Create a new gradual performance calculator for osu!mania maps.
73 pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
74 let difficulty = ManiaGradualDifficulty::new(difficulty, map)?;
75
76 Ok(Self { difficulty })
77 }
78
79 /// Process the next hit object and calculate the performance attributes
80 /// for the resulting score.
81 pub fn next(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
82 self.nth(state, 0)
83 }
84
85 /// Process all remaining hit objects and calculate the final performance
86 /// attributes.
87 pub fn last(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
88 self.nth(state, usize::MAX)
89 }
90
91 /// Process everything up the the next `n`th hit object and calculate the
92 /// performance attributes for the resulting score state.
93 ///
94 /// Note that the count is zero-indexed, so `n=0` will process 1 object,
95 /// `n=1` will process 2, and so on.
96 #[allow(clippy::missing_panics_doc)]
97 pub fn nth(&mut self, state: ManiaScoreState, n: usize) -> Option<ManiaPerformanceAttributes> {
98 let performance = self
99 .difficulty
100 .nth(n)?
101 .performance()
102 .state(state)
103 .difficulty(self.difficulty.difficulty.clone())
104 .passed_objects(self.difficulty.idx as u32)
105 .calculate()
106 .expect("no conversion required");
107
108 Some(performance)
109 }
110
111 /// Returns the amount of remaining objects.
112 #[allow(clippy::len_without_is_empty)]
113 pub fn len(&self) -> usize {
114 self.difficulty.len()
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use crate::{mania::ManiaPerformance, Beatmap};
121
122 use super::*;
123
124 #[test]
125 fn next_and_nth() {
126 let map = Beatmap::from_path("./resources/1638954.osu").unwrap();
127
128 let difficulty = Difficulty::new().mods(88); // HDHRDT
129
130 let mut gradual = ManiaGradualPerformance::new(difficulty.clone(), &map).unwrap();
131 let mut gradual_2nd = ManiaGradualPerformance::new(difficulty.clone(), &map).unwrap();
132 let mut gradual_3rd = ManiaGradualPerformance::new(difficulty.clone(), &map).unwrap();
133
134 let mut state = ManiaScoreState::default();
135
136 let hit_objects_len = map.hit_objects.len();
137
138 for i in 1.. {
139 state.misses += 1;
140
141 // Hold notes award two hitresults in lazer
142 if let Some(h) = map.hit_objects.get(i - 1) {
143 if !h.is_circle() {
144 state.n320 += 1;
145 }
146 }
147
148 let Some(next_gradual) = gradual.next(state.clone()) else {
149 assert_eq!(i, hit_objects_len + 1);
150 assert!(gradual_2nd.last(state.clone()).is_some() || hit_objects_len % 2 == 0);
151 assert!(gradual_3rd.last(state.clone()).is_some() || hit_objects_len % 3 == 0);
152 break;
153 };
154
155 if i % 2 == 0 {
156 let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
157 assert_eq!(next_gradual, next_gradual_2nd);
158 }
159
160 if i % 3 == 0 {
161 let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
162 assert_eq!(next_gradual, next_gradual_3rd);
163 }
164
165 let mut regular_calc = ManiaPerformance::new(&map)
166 .difficulty(difficulty.clone())
167 .passed_objects(i as u32)
168 .state(state.clone());
169
170 let regular_state = regular_calc.generate_state().unwrap();
171 assert_eq!(state, regular_state);
172
173 let expected = regular_calc.calculate().unwrap();
174
175 assert_eq!(next_gradual, expected);
176 }
177 }
178}