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