Skip to main content

rosu_pp/mania/difficulty/
gradual.rs

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