rosu_pp/model/beatmap/
decode.rs

1use std::{cmp, error, fmt, slice};
2
3use rosu_map::{
4    section::{
5        difficulty::{Difficulty, DifficultyKey, ParseDifficultyError},
6        events::{BreakPeriod, EventType, ParseEventTypeError},
7        general::{GameMode, GeneralKey, ParseGameModeError},
8        hit_objects::{
9            hit_samples::{HitSoundType, ParseHitSoundTypeError},
10            HitObjectType, ParseHitObjectTypeError, PathControlPoint, PathType,
11        },
12        timing_points::{ControlPoint, EffectFlags, ParseEffectFlagsError},
13    },
14    util::{KeyValue, ParseNumber, ParseNumberError, Pos, StrExt, MAX_PARSE_VALUE},
15    DecodeBeatmap, DecodeState,
16};
17
18use crate::{
19    model::{
20        control_point::{
21            difficulty_point_at, effect_point_at, DifficultyPoint, EffectPoint, TimingPoint,
22        },
23        hit_object::{HitObject, HitObjectKind, HoldNote, Slider, Spinner},
24    },
25    util::{float_ext::FloatExt, hint::unlikely, sort},
26};
27
28use super::{Beatmap, DEFAULT_SLIDER_LENIENCY};
29
30/// The state of a [`Beatmap`] for [`DecodeBeatmap`].
31pub struct BeatmapState {
32    version: i32,
33    stack_leniency: f32,
34    mode: GameMode,
35    has_approach_rate: bool,
36    difficulty: Difficulty,
37    breaks: Vec<BreakPeriod>,
38    timing_points: Vec<TimingPoint>,
39    difficulty_points: Vec<DifficultyPoint>,
40    effect_points: Vec<EffectPoint>,
41    hit_objects: Vec<HitObject>,
42    hit_sounds: Vec<HitSoundType>,
43
44    pending_control_points_time: f64,
45    pending_timing_point: Option<TimingPoint>,
46    pending_difficulty_point: Option<DifficultyPoint>,
47    pending_effect_point: Option<EffectPoint>,
48
49    curve_points: Vec<PathControlPoint>,
50    vertices: Vec<PathControlPoint>,
51    point_split: Vec<*const str>,
52}
53
54impl BeatmapState {
55    fn add_pending_point<P: Pending>(&mut self, time: f64, point: P, timing_change: bool) {
56        if time.not_eq(self.pending_control_points_time) {
57            self.flush_pending_points();
58        }
59
60        if timing_change {
61            point.push_front(self);
62        } else {
63            point.push_back(self);
64        }
65
66        self.pending_control_points_time = time;
67    }
68
69    fn flush_pending_points(&mut self) {
70        if let Some(point) = self.pending_timing_point.take() {
71            self.add_control_point(point);
72        }
73
74        if let Some(point) = self.pending_difficulty_point.take() {
75            self.add_control_point(point);
76        }
77
78        if let Some(point) = self.pending_effect_point.take() {
79            self.add_control_point(point);
80        }
81    }
82
83    fn add_control_point<P: ControlPoint<Self>>(&mut self, point: P) {
84        if !point.check_already_existing(self) {
85            point.add(self);
86        }
87    }
88
89    fn convert_path_str(&mut self, point_str: &str, offset: Pos) -> Result<(), ParseBeatmapError> {
90        let f = |this: &mut Self, point_split: &[&str]| {
91            let mut start_idx = 0;
92            let mut end_idx = 0;
93            let mut first = true;
94
95            while {
96                end_idx += 1;
97
98                end_idx < point_split.len()
99            } {
100                let is_letter = point_split[end_idx]
101                    .chars()
102                    .next()
103                    .ok_or(ParseBeatmapError::InvalidHitObjectLine)?
104                    .is_ascii_alphabetic();
105
106                if !is_letter {
107                    continue;
108                }
109
110                let end_point = point_split.get(end_idx + 1).copied();
111                this.convert_points(&point_split[start_idx..end_idx], end_point, first, offset)?;
112
113                start_idx = end_idx;
114                first = false;
115            }
116
117            if end_idx > start_idx {
118                this.convert_points(&point_split[start_idx..end_idx], None, first, offset)?;
119            }
120
121            Ok(())
122        };
123
124        self.point_split(point_str.split('|'), f)
125    }
126
127    fn convert_points(
128        &mut self,
129        points: &[&str],
130        end_point: Option<&str>,
131        first: bool,
132        offset: Pos,
133    ) -> Result<(), ParseBeatmapError> {
134        fn read_point(value: &str, start_pos: Pos) -> Result<PathControlPoint, ParseBeatmapError> {
135            let mut v = value
136                .split(':')
137                .map(|s| s.parse_with_limits(f64::from(MAX_COORDINATE_VALUE)));
138
139            let (x, y) = v
140                .next()
141                .zip(v.next())
142                .ok_or(ParseBeatmapError::InvalidHitObjectLine)?;
143
144            let pos = Pos::new(x? as i32 as f32, y? as i32 as f32);
145
146            Ok(PathControlPoint::new(pos - start_pos))
147        }
148
149        fn is_linear(p0: Pos, p1: Pos, p2: Pos) -> bool {
150            ((p1.y - p0.y) * (p2.x - p0.x)).eq((p1.x - p0.x) * (p2.y - p0.y))
151        }
152
153        let mut path_type = points
154            .first()
155            .copied()
156            .map(PathType::new_from_str)
157            .ok_or(ParseBeatmapError::InvalidHitObjectLine)?;
158
159        let read_offset = usize::from(first);
160        let readable_points = points.len() - 1;
161        let end_point_len = usize::from(end_point.is_some());
162
163        self.vertices.clear();
164        self.vertices
165            .reserve(read_offset + readable_points + end_point_len);
166
167        if first {
168            self.vertices.push(PathControlPoint::default());
169        }
170
171        for &point in points.iter().skip(1) {
172            self.vertices.push(read_point(point, offset)?);
173        }
174
175        if let Some(end_point) = end_point {
176            self.vertices.push(read_point(end_point, offset)?);
177        }
178
179        if path_type == PathType::PERFECT_CURVE {
180            if let [a, b, c] = self.vertices.as_slice() {
181                if is_linear(a.pos, b.pos, c.pos) {
182                    path_type = PathType::LINEAR;
183                }
184            } else {
185                path_type = PathType::BEZIER;
186            }
187        }
188
189        self.vertices
190            .first_mut()
191            .ok_or(ParseBeatmapError::InvalidHitObjectLine)?
192            .path_type = Some(path_type);
193
194        let mut start_idx = 0;
195        let mut end_idx = 0;
196
197        while {
198            end_idx += 1;
199
200            end_idx < self.vertices.len() - end_point_len
201        } {
202            if self.vertices[end_idx].pos != self.vertices[end_idx - 1].pos {
203                continue;
204            }
205
206            if path_type == PathType::CATMULL && end_idx > 1 {
207                continue;
208            }
209
210            if end_idx == self.vertices.len() - end_point_len - 1 {
211                continue;
212            }
213
214            self.vertices[end_idx - 1].path_type = Some(path_type);
215
216            self.curve_points.extend(&self.vertices[start_idx..end_idx]);
217
218            start_idx = end_idx + 1;
219        }
220
221        if end_idx > start_idx {
222            self.curve_points.extend(&self.vertices[start_idx..end_idx]);
223        }
224
225        Ok(())
226    }
227
228    fn point_split<'a, I, F, O>(&mut self, point_split: I, f: F) -> O
229    where
230        I: Iterator<Item = &'a str>,
231        F: FnOnce(&mut Self, &[&'a str]) -> O,
232    {
233        self.point_split.extend(point_split.map(std::ptr::from_ref));
234        let ptr = self.point_split.as_ptr();
235        let len = self.point_split.len();
236
237        // SAFETY:
238        // - *const str and &str have the same layout.
239        // - `self.point_split` is cleared after every use, ensuring that it
240        //   does not contain any invalid pointers.
241        let point_split = unsafe { slice::from_raw_parts(ptr.cast(), len) };
242        let res = f(self, point_split);
243        self.point_split.clear();
244
245        res
246    }
247}
248
249impl DecodeState for BeatmapState {
250    fn create(version: i32) -> Self {
251        Self {
252            version,
253            stack_leniency: DEFAULT_SLIDER_LENIENCY,
254            mode: GameMode::Osu,
255            has_approach_rate: false,
256            difficulty: Difficulty::default(),
257            breaks: Vec::new(),
258            timing_points: Vec::with_capacity(1),
259            difficulty_points: Vec::new(),
260            effect_points: Vec::with_capacity(32),
261            hit_objects: Vec::with_capacity(512),
262            hit_sounds: Vec::with_capacity(512),
263            pending_control_points_time: 0.0,
264            pending_timing_point: None,
265            pending_difficulty_point: None,
266            pending_effect_point: None,
267            // mean=13.11 | median=8
268            curve_points: Vec::with_capacity(8),
269            // mean=16.27 | median=8
270            vertices: Vec::with_capacity(8),
271            // mean=19.97 | median=8
272            point_split: Vec::with_capacity(8),
273        }
274    }
275}
276
277impl From<BeatmapState> for Beatmap {
278    fn from(mut state: BeatmapState) -> Self {
279        state.flush_pending_points();
280
281        let Difficulty {
282            mut hp_drain_rate,
283            mut circle_size,
284            mut overall_difficulty,
285            mut approach_rate,
286            mut slider_multiplier,
287            mut slider_tick_rate,
288        } = state.difficulty;
289
290        hp_drain_rate = hp_drain_rate.clamp(0.0, 10.0);
291
292        // * mania uses "circle size" for key count, thus different allowable range
293        circle_size = if state.mode == GameMode::Mania {
294            circle_size.clamp(1.0, 18.0)
295        } else {
296            circle_size.clamp(0.0, 10.0)
297        };
298
299        overall_difficulty = overall_difficulty.clamp(0.0, 10.0);
300        approach_rate = approach_rate.clamp(0.0, 10.0);
301
302        slider_multiplier = slider_multiplier.clamp(0.4, 3.6);
303        slider_tick_rate = slider_tick_rate.clamp(0.5, 8.0);
304
305        let mut sorter = sort::TandemSorter::new_stable(&state.hit_objects, |a, b| {
306            a.start_time.total_cmp(&b.start_time)
307        });
308
309        sorter.sort(&mut state.hit_objects);
310        sorter.sort(&mut state.hit_sounds);
311
312        if state.mode == GameMode::Mania {
313            sort::osu_legacy(&mut state.hit_objects);
314        }
315
316        Beatmap {
317            version: state.version,
318            is_convert: false,
319            stack_leniency: state.stack_leniency,
320            mode: state.mode,
321            ar: approach_rate,
322            cs: circle_size,
323            hp: hp_drain_rate,
324            od: overall_difficulty,
325            slider_multiplier,
326            slider_tick_rate,
327            breaks: state.breaks,
328            timing_points: state.timing_points,
329            difficulty_points: state.difficulty_points,
330            effect_points: state.effect_points,
331            hit_objects: state.hit_objects,
332            hit_sounds: state.hit_sounds,
333        }
334    }
335}
336
337/// All the ways that parsing a [`Beatmap`] can fail.
338#[derive(Debug)]
339pub enum ParseBeatmapError {
340    EffectFlags(ParseEffectFlagsError),
341    EventType(ParseEventTypeError),
342    HitObjectType(ParseHitObjectTypeError),
343    HitSoundType(ParseHitSoundTypeError),
344    InvalidEventLine,
345    InvalidRepeatCount,
346    InvalidTimingPointLine,
347    InvalidHitObjectLine,
348    Mode(ParseGameModeError),
349    Number(ParseNumberError),
350    TimeSignature,
351    TimingControlPointNaN,
352    UnknownHitObjectType,
353}
354
355impl error::Error for ParseBeatmapError {
356    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
357        match self {
358            ParseBeatmapError::EffectFlags(err) => Some(err),
359            ParseBeatmapError::EventType(err) => Some(err),
360            ParseBeatmapError::HitObjectType(err) => Some(err),
361            ParseBeatmapError::HitSoundType(err) => Some(err),
362            ParseBeatmapError::Mode(err) => Some(err),
363            ParseBeatmapError::Number(err) => Some(err),
364            ParseBeatmapError::InvalidEventLine
365            | ParseBeatmapError::InvalidRepeatCount
366            | ParseBeatmapError::InvalidTimingPointLine
367            | ParseBeatmapError::InvalidHitObjectLine
368            | ParseBeatmapError::TimeSignature
369            | ParseBeatmapError::TimingControlPointNaN
370            | ParseBeatmapError::UnknownHitObjectType => None,
371        }
372    }
373}
374
375impl fmt::Display for ParseBeatmapError {
376    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377        let s = match self {
378            Self::EffectFlags(_) => "failed to parse effect flags",
379            Self::EventType(_) => "failed to parse event type",
380            Self::HitObjectType(_) => "failed to parse hit object type",
381            Self::HitSoundType(_) => "failed to parse hit sound type",
382            Self::InvalidEventLine => "invalid event line",
383            Self::InvalidRepeatCount => "repeat count is way too high",
384            Self::InvalidTimingPointLine => "invalid timing point line",
385            Self::InvalidHitObjectLine => "invalid hit object line",
386            Self::Mode(_) => "failed to parse mode",
387            Self::Number(_) => "failed to parse number",
388            Self::TimeSignature => "invalid time signature, must be positive integer",
389            Self::TimingControlPointNaN => "beat length cannot be NaN in a timing control point",
390            Self::UnknownHitObjectType => "unknown hit object type",
391        };
392
393        f.write_str(s)
394    }
395}
396
397impl From<ParseEffectFlagsError> for ParseBeatmapError {
398    fn from(err: ParseEffectFlagsError) -> Self {
399        Self::EffectFlags(err)
400    }
401}
402
403impl From<ParseEventTypeError> for ParseBeatmapError {
404    fn from(err: ParseEventTypeError) -> Self {
405        Self::EventType(err)
406    }
407}
408
409impl From<ParseHitObjectTypeError> for ParseBeatmapError {
410    fn from(err: ParseHitObjectTypeError) -> Self {
411        Self::HitObjectType(err)
412    }
413}
414
415impl From<ParseHitSoundTypeError> for ParseBeatmapError {
416    fn from(err: ParseHitSoundTypeError) -> Self {
417        Self::HitSoundType(err)
418    }
419}
420
421impl From<ParseGameModeError> for ParseBeatmapError {
422    fn from(err: ParseGameModeError) -> Self {
423        Self::Mode(err)
424    }
425}
426
427impl From<ParseNumberError> for ParseBeatmapError {
428    fn from(err: ParseNumberError) -> Self {
429        Self::Number(err)
430    }
431}
432
433impl From<ParseDifficultyError> for ParseBeatmapError {
434    fn from(err: ParseDifficultyError) -> Self {
435        match err {
436            ParseDifficultyError::Number(err) => Self::Number(err),
437        }
438    }
439}
440
441const MAX_COORDINATE_VALUE: i32 = 131_072;
442
443impl DecodeBeatmap for Beatmap {
444    type Error = ParseBeatmapError;
445    type State = BeatmapState;
446
447    fn parse_general(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
448        let Ok(KeyValue { key, value }) = KeyValue::parse(line.trim_comment()) else {
449            return Ok(());
450        };
451
452        match key {
453            GeneralKey::StackLeniency => state.stack_leniency = value.parse_num()?,
454            GeneralKey::Mode => state.mode = value.parse()?,
455            _ => {}
456        }
457
458        Ok(())
459    }
460
461    fn parse_editor(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
462        Ok(())
463    }
464
465    fn parse_metadata(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
466        Ok(())
467    }
468
469    fn parse_difficulty(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
470        let Ok(KeyValue { key, value }) = KeyValue::parse(line.trim_comment()) else {
471            return Ok(());
472        };
473
474        match key {
475            DifficultyKey::HPDrainRate => state.difficulty.hp_drain_rate = value.parse_num()?,
476            DifficultyKey::CircleSize => state.difficulty.circle_size = value.parse_num()?,
477            DifficultyKey::OverallDifficulty => {
478                state.difficulty.overall_difficulty = value.parse_num()?;
479
480                if !state.has_approach_rate {
481                    state.difficulty.approach_rate = state.difficulty.overall_difficulty;
482                }
483            }
484            DifficultyKey::ApproachRate => {
485                state.difficulty.approach_rate = value.parse_num()?;
486                state.has_approach_rate = true;
487            }
488            DifficultyKey::SliderMultiplier => {
489                state.difficulty.slider_multiplier = f64::parse(value)?;
490            }
491            DifficultyKey::SliderTickRate => state.difficulty.slider_tick_rate = f64::parse(value)?,
492        }
493
494        Ok(())
495    }
496
497    fn parse_events(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
498        let mut split = line.trim_comment().split(',');
499
500        let event_type: EventType = split
501            .next()
502            .ok_or(ParseBeatmapError::InvalidEventLine)?
503            .parse()?;
504
505        if event_type == EventType::Break {
506            let Some((start_time, end_time)) = split.next().zip(split.next()) else {
507                return Err(ParseBeatmapError::InvalidEventLine);
508            };
509
510            let start_time = f64::parse(start_time)?;
511            let end_time = start_time.max(f64::parse(end_time)?);
512
513            state.breaks.push(BreakPeriod {
514                start_time,
515                end_time,
516            });
517        }
518
519        Ok(())
520    }
521
522    fn parse_timing_points(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
523        let mut split = line.trim_comment().split(',');
524
525        let (time, beat_len) = split
526            .next()
527            .zip(split.next())
528            .ok_or(ParseBeatmapError::InvalidTimingPointLine)?;
529
530        let time = time.parse_num::<f64>()?;
531
532        // Manual `str::parse_num::<f64>` so that NaN does not cause an error
533        let beat_len = beat_len
534            .trim()
535            .parse::<f64>()
536            .map_err(ParseNumberError::InvalidFloat)?;
537
538        if unlikely(beat_len < f64::from(-MAX_PARSE_VALUE)) {
539            return Err(ParseNumberError::NumberUnderflow.into());
540        } else if unlikely(beat_len > f64::from(MAX_PARSE_VALUE)) {
541            return Err(ParseNumberError::NumberOverflow.into());
542        }
543
544        let speed_multiplier = if beat_len < 0.0 {
545            100.0 / -beat_len
546        } else {
547            1.0
548        };
549
550        if let Some(numerator) = split.next() {
551            if unlikely(i32::parse(numerator)? < 1) {
552                return Err(ParseBeatmapError::TimeSignature);
553            }
554        }
555
556        let _ = split.next(); // sample set
557        let _ = split.next(); // custom sample bank
558        let _ = split.next(); // sample volume
559
560        let timing_change = split
561            .next()
562            .is_none_or(|next| matches!(next.chars().next(), Some('1')));
563
564        let kiai = split
565            .next()
566            .map(str::parse::<EffectFlags>)
567            .transpose()?
568            .is_some_and(|flags| flags.has_flag(EffectFlags::KIAI));
569
570        if timing_change {
571            if unlikely(beat_len.is_nan()) {
572                return Err(ParseBeatmapError::TimingControlPointNaN);
573            }
574
575            let timing = TimingPoint::new(time, beat_len);
576            state.add_pending_point(time, timing, timing_change);
577        }
578
579        let difficulty = DifficultyPoint::new(time, beat_len, speed_multiplier);
580        state.add_pending_point(time, difficulty, timing_change);
581
582        let mut effect = EffectPoint::new(time, kiai);
583
584        if matches!(state.mode, GameMode::Taiko | GameMode::Mania) {
585            effect.scroll_speed = speed_multiplier.clamp(0.01, 10.0);
586        }
587
588        state.add_pending_point(time, effect, timing_change);
589
590        state.pending_control_points_time = time;
591
592        Ok(())
593    }
594
595    fn parse_colors(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
596        Ok(())
597    }
598
599    #[allow(clippy::too_many_lines)]
600    fn parse_hit_objects(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
601        let mut split = line.trim_comment().split(',');
602
603        let (Some(x), Some(y), Some(start_time), Some(kind), Some(sound_type)) = (
604            split.next(),
605            split.next(),
606            split.next(),
607            split.next(),
608            split.next(),
609        ) else {
610            return Err(ParseBeatmapError::InvalidHitObjectLine);
611        };
612
613        let pos = Pos {
614            x: x.parse_with_limits(MAX_COORDINATE_VALUE as f32)? as i32 as f32,
615            y: y.parse_with_limits(MAX_COORDINATE_VALUE as f32)? as i32 as f32,
616        };
617
618        let start_time = f64::parse(start_time)?;
619        let hit_object_type: HitObjectType = kind.parse()?;
620
621        let mut sound: HitSoundType = sound_type.parse()?;
622
623        let mut parse_custom_sound = |bank_info: Option<&str>| {
624            let mut split = match bank_info {
625                Some(s) if !s.is_empty() => s.split(':'),
626                _ => return Ok::<_, ParseNumberError>(()),
627            };
628
629            let _ = split.next().map(i32::parse).transpose()?; // normal bank
630            let _ = split.next().map(i32::parse).transpose()?; // additional bank
631            let _ = split.next().map(i32::parse).transpose()?; // custom sample bank
632            let _ = split.next().map(i32::parse).transpose()?; // volume
633
634            // filename
635            match split.next() {
636                None | Some("") => {}
637                // Relevant maps:
638                //   - /b/244784 at 43374
639                Some(_) => sound &= !HitSoundType::NORMAL,
640            }
641
642            Ok(())
643        };
644
645        let kind = if hit_object_type.has_flag(HitObjectType::CIRCLE) {
646            parse_custom_sound(split.next())?;
647
648            HitObjectKind::Circle
649        } else if hit_object_type.has_flag(HitObjectType::SLIDER) {
650            let (point_str, repeat_count) = split
651                .next()
652                .zip(split.next())
653                .ok_or(ParseBeatmapError::InvalidHitObjectLine)?;
654
655            let mut len = None;
656
657            let repeats = repeat_count.parse_num::<i32>()?;
658
659            if unlikely(repeats > 9000) {
660                return Err(ParseBeatmapError::InvalidRepeatCount);
661            }
662
663            let repeats = cmp::max(0, repeats - 1) as usize;
664
665            if let Some(next) = split.next() {
666                let new_len = next
667                    .parse_with_limits(f64::from(MAX_COORDINATE_VALUE))?
668                    .max(0.0);
669
670                if new_len.not_eq(0.0) {
671                    len = Some(new_len);
672                }
673            }
674
675            let node_sounds_str = split.next();
676
677            let _ = split.next(); // node banks
678            parse_custom_sound(split.next())?;
679
680            let mut node_sounds = vec![sound; repeats + 2].into_boxed_slice();
681
682            if let Some(sounds) = node_sounds_str {
683                sounds
684                    .split('|')
685                    .map(|s| s.parse().unwrap_or_default())
686                    .zip(node_sounds.iter_mut())
687                    .for_each(|(parsed, sound)| *sound = parsed);
688            }
689
690            state.convert_path_str(point_str, pos)?;
691            let mut control_points = Vec::with_capacity(state.curve_points.len());
692            control_points.append(&mut state.curve_points);
693
694            let slider = Slider {
695                expected_dist: len,
696                repeats,
697                control_points: control_points.into_boxed_slice(),
698                node_sounds,
699            };
700
701            HitObjectKind::Slider(slider)
702        } else if hit_object_type.has_flag(HitObjectType::SPINNER) {
703            let end_time = split
704                .next()
705                .ok_or(ParseBeatmapError::InvalidHitObjectLine)?
706                .parse_num::<f64>()?;
707
708            parse_custom_sound(split.next())?;
709
710            let duration = (end_time - start_time).max(0.0);
711
712            HitObjectKind::Spinner(Spinner { duration })
713        } else if hit_object_type.has_flag(HitObjectType::HOLD) {
714            let end_time = if let Some(s) = split.next().filter(|s| !s.is_empty()) {
715                let (end_time, bank_info) = s
716                    .split_once(':')
717                    .ok_or(ParseBeatmapError::InvalidHitObjectLine)?;
718
719                parse_custom_sound(Some(bank_info))?;
720
721                end_time.parse_num::<f64>()?.max(start_time)
722            } else {
723                start_time
724            };
725
726            let duration = end_time - start_time;
727
728            HitObjectKind::Hold(HoldNote { duration })
729        } else {
730            return Err(ParseBeatmapError::UnknownHitObjectType);
731        };
732
733        state.hit_objects.push(HitObject {
734            pos,
735            start_time,
736            kind,
737        });
738        state.hit_sounds.push(sound);
739
740        Ok(())
741    }
742
743    fn parse_variables(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
744        Ok(())
745    }
746
747    fn parse_catch_the_beat(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
748        Ok(())
749    }
750
751    fn parse_mania(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
752        Ok(())
753    }
754}
755
756trait Pending: Sized {
757    fn pending(state: &mut BeatmapState) -> &mut Option<Self>;
758
759    fn push_front(self, state: &mut BeatmapState) {
760        let pending = Self::pending(state);
761
762        if pending.is_none() {
763            *pending = Some(self);
764        }
765    }
766
767    fn push_back(self, state: &mut BeatmapState) {
768        *Self::pending(state) = Some(self);
769    }
770}
771
772impl Pending for TimingPoint {
773    fn pending(state: &mut BeatmapState) -> &mut Option<Self> {
774        &mut state.pending_timing_point
775    }
776}
777
778impl Pending for DifficultyPoint {
779    fn pending(state: &mut BeatmapState) -> &mut Option<Self> {
780        &mut state.pending_difficulty_point
781    }
782}
783
784impl Pending for EffectPoint {
785    fn pending(state: &mut BeatmapState) -> &mut Option<Self> {
786        &mut state.pending_effect_point
787    }
788}
789
790impl ControlPoint<BeatmapState> for TimingPoint {
791    fn check_already_existing(&self, _: &BeatmapState) -> bool {
792        false
793    }
794
795    fn add(self, state: &mut BeatmapState) {
796        match state
797            .timing_points
798            .binary_search_by(|probe| probe.time.total_cmp(&self.time))
799        {
800            Err(i) => state.timing_points.insert(i, self),
801            Ok(i) => state.timing_points[i] = self,
802        }
803    }
804}
805
806impl ControlPoint<BeatmapState> for DifficultyPoint {
807    fn check_already_existing(&self, state: &BeatmapState) -> bool {
808        match difficulty_point_at(&state.difficulty_points, self.time) {
809            Some(existing) => self.is_redundant(existing),
810            None => self.is_redundant(&DifficultyPoint::default()),
811        }
812    }
813
814    fn add(self, state: &mut BeatmapState) {
815        match state
816            .difficulty_points
817            .binary_search_by(|probe| probe.time.total_cmp(&self.time))
818        {
819            Err(i) => state.difficulty_points.insert(i, self),
820            Ok(i) => state.difficulty_points[i] = self,
821        }
822    }
823}
824
825impl ControlPoint<BeatmapState> for EffectPoint {
826    fn check_already_existing(&self, state: &BeatmapState) -> bool {
827        self.check_already_existing(&state.effect_points)
828    }
829
830    fn add(self, state: &mut BeatmapState) {
831        self.add(&mut state.effect_points);
832    }
833}
834
835// osu!taiko conversion mutates the list of effect points
836impl ControlPoint<Vec<EffectPoint>> for EffectPoint {
837    fn check_already_existing(&self, effect_points: &Vec<EffectPoint>) -> bool {
838        match effect_point_at(effect_points, self.time) {
839            Some(existing) => self.is_redundant(existing),
840            None => self.is_redundant(&EffectPoint::default()),
841        }
842    }
843
844    fn add(self, effect_points: &mut Vec<EffectPoint>) {
845        match effect_points.binary_search_by(|probe| probe.time.total_cmp(&self.time)) {
846            Err(i) => effect_points.insert(i, self),
847            Ok(i) => effect_points[i] = self,
848        }
849    }
850}