Skip to main content

rosu_pp/catch/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    catch::{
9        CatchDifficultyAttributes,
10        attributes::{GradualObjectCount, ObjectCountBuilder},
11        catcher::Catcher,
12        convert::convert_objects,
13    },
14    model::mode::ConvertError,
15};
16
17use super::{
18    CatchDifficultySetup, DifficultyValues, object::CatchDifficultyObject,
19    skills::movement::Movement,
20};
21
22/// Gradually calculate the difficulty attributes of an osu!catch map.
23///
24/// Note that this struct implements [`Iterator`].
25/// On every call of [`Iterator::next`], the map's next fruit or droplet
26/// will be processed and the [`CatchDifficultyAttributes`] will be updated and
27/// returned.
28///
29/// Note that it does not return attributes after a tiny droplet. Only for
30/// fruits and droplets.
31///
32/// If you want to calculate performance attributes, use
33/// [`CatchGradualPerformance`] instead.
34///
35/// # Example
36///
37/// ```
38/// use rosu_pp::{Beatmap, Difficulty};
39/// use rosu_pp::catch::{Catch, CatchGradualDifficulty};
40///
41/// let map = Beatmap::from_path("./resources/2118524.osu").unwrap();
42///
43/// let difficulty = Difficulty::new().mods(64); // DT
44/// let mut iter = CatchGradualDifficulty::new(difficulty, &map).unwrap();
45///
46/// // the difficulty of the map after the first hit object
47/// let attrs1 = iter.next();
48/// // ... after the second hit object
49/// let attrs2 = iter.next();
50///
51/// // Remaining hit objects
52/// for difficulty in iter {
53///     // ...
54/// }
55/// ```
56///
57/// [`CatchGradualPerformance`]: crate::catch::CatchGradualPerformance
58pub struct CatchGradualDifficulty {
59    pub(crate) idx: usize,
60    pub(crate) difficulty: Difficulty,
61    attrs: CatchDifficultyAttributes,
62    /// The delta of object counts after each palpable object
63    count: Vec<GradualObjectCount>,
64    diff_objects: Box<[CatchDifficultyObject]>,
65    movement: Movement,
66}
67
68impl CatchGradualDifficulty {
69    /// Create a new difficulty attributes iterator for osu!catch maps.
70    pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
71        let map = super::prepare_map(&difficulty, map)?;
72
73        Ok(new(difficulty, &map))
74    }
75
76    /// Same as [`CatchGradualDifficulty::new`] but verifies that the map is not
77    /// suspicious.
78    pub fn checked_new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, CalculateError> {
79        let map = super::prepare_map(&difficulty, map)?;
80        map.check_suspicion()?;
81
82        Ok(new(difficulty, &map))
83    }
84}
85
86fn new(difficulty: Difficulty, map: &Beatmap) -> CatchGradualDifficulty {
87    debug_assert_eq!(map.mode, GameMode::Catch);
88
89    let clock_rate = difficulty.get_clock_rate();
90
91    let CatchDifficultySetup { map_attrs, attrs } = CatchDifficultySetup::new(&difficulty, map);
92
93    let hr_offsets = difficulty.get_hardrock_offsets();
94    let reflection = difficulty.get_mods().reflection();
95    let mut count = ObjectCountBuilder::new_gradual();
96
97    let palpable_objects = convert_objects(map, &mut count, reflection, hr_offsets, map_attrs.cs());
98
99    let mut half_catcher_width = Catcher::calculate_catch_width(map_attrs.cs()) * 0.5;
100    half_catcher_width *= 1.0 - ((map_attrs.cs() - 5.5).max(0.0) * 0.0625);
101
102    let diff_objects = DifficultyValues::create_difficulty_objects(
103        clock_rate,
104        half_catcher_width,
105        palpable_objects.iter(),
106    );
107
108    let count = count.into_gradual();
109    let movement = Movement::new(clock_rate);
110
111    CatchGradualDifficulty {
112        idx: 0,
113        difficulty,
114        attrs,
115        count,
116        diff_objects,
117        movement,
118    }
119}
120
121impl Iterator for CatchGradualDifficulty {
122    type Item = CatchDifficultyAttributes;
123
124    fn next(&mut self) -> Option<Self::Item> {
125        // The first difficulty object belongs to the second palpable object
126        // since each difficulty object requires the current and the last note.
127        // Hence, if we're still on the first object, we don't have a difficulty
128        // object yet and just skip processing.
129        if self.idx > 0 {
130            let curr = self.diff_objects.get(self.idx - 1)?;
131            self.movement.process(curr, &self.diff_objects);
132        } else if self.count.is_empty() {
133            return None;
134        }
135
136        self.attrs.add_object_count(self.count[self.idx]);
137        self.idx += 1;
138
139        let mut attrs = self.attrs.clone();
140
141        let movement = self.movement.cloned_difficulty_value();
142        DifficultyValues::eval(&mut attrs, movement);
143
144        Some(attrs)
145    }
146
147    fn size_hint(&self) -> (usize, Option<usize>) {
148        let len = self.len();
149
150        (len, Some(len))
151    }
152
153    fn nth(&mut self, n: usize) -> Option<Self::Item> {
154        let skip_iter = self.diff_objects.iter().skip(self.idx.saturating_sub(1));
155
156        let mut take = cmp::min(n, self.len().saturating_sub(1));
157
158        // The first palpable object has no difficulty object
159        if self.idx == 0 && take > 0 {
160            take -= 1;
161            self.attrs.add_object_count(self.count[self.idx]);
162            self.idx += 1;
163        }
164
165        for curr in skip_iter.take(take) {
166            self.movement.process(curr, &self.diff_objects);
167
168            self.attrs.add_object_count(self.count[self.idx]);
169            self.idx += 1;
170        }
171
172        self.next()
173    }
174}
175
176impl ExactSizeIterator for CatchGradualDifficulty {
177    fn len(&self) -> usize {
178        self.diff_objects.len() + 1 - self.idx
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use crate::{Beatmap, catch::Catch};
185
186    use super::*;
187
188    #[test]
189    fn empty() {
190        let map = Beatmap::from_bytes(&[]).unwrap();
191        let mut gradual = CatchGradualDifficulty::new(Difficulty::new(), &map).unwrap();
192        assert!(gradual.next().is_none());
193    }
194
195    #[test]
196    fn next_and_nth() {
197        let map = Beatmap::from_path("./resources/2118524.osu").unwrap();
198
199        let difficulty = Difficulty::new();
200
201        let mut gradual = CatchGradualDifficulty::new(difficulty.clone(), &map).unwrap();
202        let mut gradual_2nd = CatchGradualDifficulty::new(difficulty.clone(), &map).unwrap();
203        let mut gradual_3rd = CatchGradualDifficulty::new(difficulty.clone(), &map).unwrap();
204
205        for i in 1.. {
206            let Some(next_gradual) = gradual.next() else {
207                assert_eq!(i, 731);
208                assert!(gradual_2nd.last().is_none()); // 730 % 2 == 0
209                assert!(gradual_3rd.last().is_some()); // 730 % 3 == 1
210                break;
211            };
212
213            if i % 2 == 0 {
214                let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
215                assert_eq!(next_gradual, next_gradual_2nd);
216            }
217
218            if i % 3 == 0 {
219                let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
220                assert_eq!(next_gradual, next_gradual_3rd);
221            }
222
223            let expected = difficulty
224                .clone()
225                .passed_objects(i as u32)
226                .calculate_for_mode::<Catch>(&map)
227                .unwrap();
228
229            assert_eq!(next_gradual, expected);
230        }
231    }
232}