rosu_pp/mania/difficulty/
gradual.rs1use 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
18pub 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 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 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 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 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}