rosu_pp/mania/difficulty/
gradual.rs

1use std::cmp;
2
3use rosu_map::section::general::GameMode;
4
5use crate::{
6    any::difficulty::skills::StrainSkill,
7    mania::{convert, object::ObjectParams},
8    model::{hit_object::HitObject, mode::ConvertError},
9    Beatmap, Difficulty,
10};
11
12use super::{
13    object::ManiaDifficultyObject, skills::strain::Strain, DifficultyValues,
14    ManiaDifficultyAttributes, ManiaObject, DIFFICULTY_MULTIPLIER,
15};
16
17/// Gradually calculate the difficulty attributes of an osu!mania map.
18///
19/// Note that this struct implements [`Iterator`].
20/// On every call of [`Iterator::next`], the map's next hit object will
21/// be processed and the [`ManiaDifficultyAttributes`] will be updated and returned.
22///
23/// If you want to calculate performance attributes, use
24/// [`ManiaGradualPerformance`] instead.
25///
26/// # Example
27///
28/// ```
29/// use rosu_pp::{Beatmap, Difficulty};
30/// use rosu_pp::mania::ManiaGradualDifficulty;
31///
32/// let map = Beatmap::from_path("./resources/1638954.osu").unwrap();
33///
34/// let difficulty = Difficulty::new().mods(64); // DT
35/// let mut iter = ManiaGradualDifficulty::new(difficulty, &map).unwrap();
36///
37/// // the difficulty of the map after the first hit object
38/// let attrs1 = iter.next();
39/// // ... after the second hit object
40/// let attrs2 = iter.next();
41///
42/// // Remaining hit objects
43/// for difficulty in iter {
44///     // ...
45/// }
46/// ```
47///
48/// [`ManiaGradualPerformance`]: crate::mania::ManiaGradualPerformance
49pub struct ManiaGradualDifficulty {
50    pub(crate) idx: usize,
51    pub(crate) difficulty: Difficulty,
52    objects_is_circle: Box<[bool]>,
53    is_convert: bool,
54    strain: Strain,
55    diff_objects: Box<[ManiaDifficultyObject]>,
56    note_state: NoteState,
57}
58
59#[derive(Default)]
60struct NoteState {
61    curr_combo: u32,
62    n_hold_notes: u32,
63}
64
65impl ManiaGradualDifficulty {
66    /// Create a new difficulty attributes iterator for osu!mania maps.
67    pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
68        let mut map = map.convert_ref(GameMode::Mania, difficulty.get_mods())?;
69
70        if difficulty.get_mods().ho() {
71            convert::apply_hold_off_to_beatmap(map.to_mut());
72        }
73
74        if difficulty.get_mods().invert() {
75            convert::apply_invert_to_beatmap(map.to_mut());
76        }
77
78        if let Some(seed) = difficulty.get_mods().random_seed() {
79            convert::apply_random_to_beatmap(map.to_mut(), seed);
80        }
81
82        let take = difficulty.get_passed_objects();
83        let total_columns = map.cs.round_ties_even().max(1.0);
84        let clock_rate = difficulty.get_clock_rate();
85        let mut params = ObjectParams::new(&map);
86
87        let mania_objects = map
88            .hit_objects
89            .iter()
90            .map(|h| ManiaObject::new(h, total_columns, &mut params))
91            .take(take);
92
93        let diff_objects = DifficultyValues::create_difficulty_objects(clock_rate, mania_objects);
94
95        let strain = Strain::new(total_columns as usize);
96
97        let mut note_state = NoteState::default();
98
99        let objects_is_circle: Box<[_]> =
100            map.hit_objects.iter().map(HitObject::is_circle).collect();
101
102        if let Some(h) = map.hit_objects.first() {
103            let hit_object = ManiaObject::new(h, total_columns, &mut params);
104
105            increment_combo_raw(
106                objects_is_circle[0],
107                hit_object.start_time,
108                hit_object.end_time,
109                &mut note_state,
110            );
111        }
112
113        Ok(Self {
114            idx: 0,
115            difficulty,
116            objects_is_circle,
117            is_convert: map.is_convert,
118            strain,
119            diff_objects,
120            note_state,
121        })
122    }
123}
124
125impl Iterator for ManiaGradualDifficulty {
126    type Item = ManiaDifficultyAttributes;
127
128    fn next(&mut self) -> Option<Self::Item> {
129        // The first difficulty object belongs to the second note since each
130        // difficulty object requires the current and the last note. Hence, if
131        // we're still on the first object, we don't have a difficulty object
132        // yet and just skip processing.
133        if self.idx > 0 {
134            let curr = self.diff_objects.get(self.idx - 1)?;
135            self.strain.process(curr, &self.diff_objects);
136
137            let is_circle = self.objects_is_circle[self.idx];
138            increment_combo(
139                is_circle,
140                curr,
141                &mut self.note_state,
142                self.difficulty.get_clock_rate(),
143            );
144        } else if self.objects_is_circle.is_empty() {
145            return None;
146        }
147
148        self.idx += 1;
149
150        Some(ManiaDifficultyAttributes {
151            stars: self.strain.cloned_difficulty_value() * DIFFICULTY_MULTIPLIER,
152            max_combo: self.note_state.curr_combo,
153            n_objects: self.idx as u32,
154            n_hold_notes: self.note_state.n_hold_notes,
155            is_convert: self.is_convert,
156        })
157    }
158
159    fn size_hint(&self) -> (usize, Option<usize>) {
160        let len = self.len();
161
162        (len, Some(len))
163    }
164
165    fn nth(&mut self, n: usize) -> Option<Self::Item> {
166        let skip_iter = self
167            .diff_objects
168            .iter()
169            .zip(self.objects_is_circle.iter().skip(1))
170            .skip(self.idx.saturating_sub(1));
171
172        let mut take = cmp::min(n, self.len().saturating_sub(1));
173
174        // The first note has no difficulty object
175        if self.idx == 0 && take > 0 {
176            take -= 1;
177            self.idx += 1;
178        }
179
180        let clock_rate = self.difficulty.get_clock_rate();
181
182        for (curr, is_circle) in skip_iter.take(take) {
183            increment_combo(*is_circle, curr, &mut self.note_state, clock_rate);
184            self.strain.process(curr, &self.diff_objects);
185            self.idx += 1;
186        }
187
188        self.next()
189    }
190}
191
192impl ExactSizeIterator for ManiaGradualDifficulty {
193    fn len(&self) -> usize {
194        self.diff_objects.len() + 1 - self.idx
195    }
196}
197
198fn increment_combo(
199    is_circle: bool,
200    diff_obj: &ManiaDifficultyObject,
201    state: &mut NoteState,
202    clock_rate: f64,
203) {
204    increment_combo_raw(
205        is_circle,
206        diff_obj.start_time * clock_rate,
207        diff_obj.end_time * clock_rate,
208        state,
209    );
210}
211
212fn increment_combo_raw(is_circle: bool, start_time: f64, end_time: f64, state: &mut NoteState) {
213    if is_circle {
214        state.curr_combo += 1;
215    } else {
216        state.curr_combo += 1 + ((end_time - start_time) / 100.0) as u32;
217        state.n_hold_notes += 1;
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use crate::{mania::Mania, Beatmap};
224
225    use super::*;
226
227    #[test]
228    fn empty() {
229        let map = Beatmap::from_bytes(&[]).unwrap();
230        let mut gradual = ManiaGradualDifficulty::new(Difficulty::new(), &map).unwrap();
231        assert!(gradual.next().is_none());
232    }
233
234    #[test]
235    fn next_and_nth() {
236        let map = Beatmap::from_path("./resources/1638954.osu").unwrap();
237
238        let difficulty = Difficulty::new();
239
240        let mut gradual = ManiaGradualDifficulty::new(difficulty.clone(), &map).unwrap();
241        let mut gradual_2nd = ManiaGradualDifficulty::new(difficulty.clone(), &map).unwrap();
242        let mut gradual_3rd = ManiaGradualDifficulty::new(difficulty.clone(), &map).unwrap();
243
244        let hit_objects_len = map.hit_objects.len();
245
246        for i in 1.. {
247            let Some(next_gradual) = gradual.next() else {
248                assert_eq!(i, hit_objects_len + 1);
249                assert!(gradual_2nd.last().is_some() || hit_objects_len % 2 == 0);
250                assert!(gradual_3rd.last().is_some() || hit_objects_len % 3 == 0);
251                break;
252            };
253
254            if i % 2 == 0 {
255                let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
256                assert_eq!(next_gradual, next_gradual_2nd);
257            }
258
259            if i % 3 == 0 {
260                let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
261                assert_eq!(next_gradual, next_gradual_3rd);
262            }
263
264            let expected = difficulty
265                .clone()
266                .passed_objects(i as u32)
267                .calculate_for_mode::<Mania>(&map)
268                .unwrap();
269
270            assert_eq!(next_gradual, expected);
271        }
272    }
273}