rosu_map/section/hit_objects/
decode.rs

1use std::{cmp, ptr, slice};
2
3use crate::{
4    decode::{DecodeBeatmap, DecodeState},
5    section::{
6        difficulty::{Difficulty, DifficultyState, ParseDifficultyError},
7        events::{BreakPeriod, Events, EventsState, ParseEventsError},
8        general::{CountdownType, GameMode},
9        hit_objects::{slider::path_type::PathType, CurveBuffers, BASE_SCORING_DIST},
10        timing_points::{
11            ControlPoints, DifficultyPoint, ParseTimingPointsError, SamplePoint, TimingPoint,
12            TimingPoints, TimingPointsState,
13        },
14    },
15    util::{ParseNumber, ParseNumberError, Pos, StrExt},
16    Beatmap,
17};
18
19use super::{
20    hit_samples::{
21        HitSoundType, ParseHitSoundTypeError, ParseSampleBankInfoError, SampleBank, SampleBankInfo,
22    },
23    HitObject, HitObjectCircle, HitObjectHold, HitObjectKind, HitObjectSlider, HitObjectSpinner,
24    HitObjectType, ParseHitObjectTypeError, PathControlPoint, SliderPath,
25};
26
27/// Struct containing all data from a `.osu` file's `[HitObjects]`, `[Events]`,
28/// `[Difficulty]`, `[TimingPoints]` and `[General]` section.
29#[derive(Clone, Debug, PartialEq)]
30pub struct HitObjects {
31    // General
32    pub audio_file: String,
33    pub audio_lead_in: f64,
34    pub preview_time: i32,
35    pub default_sample_bank: SampleBank,
36    pub default_sample_volume: i32,
37    pub stack_leniency: f32,
38    pub mode: GameMode,
39    pub letterbox_in_breaks: bool,
40    pub special_style: bool,
41    pub widescreen_storyboard: bool,
42    pub epilepsy_warning: bool,
43    pub samples_match_playback_rate: bool,
44    pub countdown: CountdownType,
45    pub countdown_offset: i32,
46
47    // Difficulty
48    pub hp_drain_rate: f32,
49    pub circle_size: f32,
50    pub overall_difficulty: f32,
51    pub approach_rate: f32,
52    pub slider_multiplier: f64,
53    pub slider_tick_rate: f64,
54
55    // Events
56    pub background_file: String,
57    pub breaks: Vec<BreakPeriod>,
58
59    // TimingPoints
60    pub control_points: ControlPoints,
61
62    // HitObjects
63    pub hit_objects: Vec<HitObject>,
64}
65
66impl Default for HitObjects {
67    fn default() -> Self {
68        let difficulty = Difficulty::default();
69        let events = Events::default();
70        let timing_points = TimingPoints::default();
71
72        Self {
73            audio_file: timing_points.audio_file,
74            audio_lead_in: timing_points.audio_lead_in,
75            preview_time: timing_points.preview_time,
76            default_sample_bank: timing_points.default_sample_bank,
77            default_sample_volume: timing_points.default_sample_volume,
78            stack_leniency: timing_points.stack_leniency,
79            mode: timing_points.mode,
80            letterbox_in_breaks: timing_points.letterbox_in_breaks,
81            special_style: timing_points.special_style,
82            widescreen_storyboard: timing_points.widescreen_storyboard,
83            epilepsy_warning: timing_points.epilepsy_warning,
84            samples_match_playback_rate: timing_points.samples_match_playback_rate,
85            countdown: timing_points.countdown,
86            countdown_offset: timing_points.countdown_offset,
87            hp_drain_rate: difficulty.hp_drain_rate,
88            circle_size: difficulty.circle_size,
89            overall_difficulty: difficulty.overall_difficulty,
90            approach_rate: difficulty.approach_rate,
91            slider_multiplier: difficulty.slider_multiplier,
92            slider_tick_rate: difficulty.slider_tick_rate,
93            background_file: events.background_file,
94            breaks: events.breaks,
95            control_points: timing_points.control_points,
96            hit_objects: Vec::default(),
97        }
98    }
99}
100
101impl From<HitObjects> for Beatmap {
102    fn from(hit_objects: HitObjects) -> Self {
103        Self {
104            audio_file: hit_objects.audio_file,
105            audio_lead_in: hit_objects.audio_lead_in,
106            preview_time: hit_objects.preview_time,
107            default_sample_bank: hit_objects.default_sample_bank,
108            default_sample_volume: hit_objects.default_sample_volume,
109            stack_leniency: hit_objects.stack_leniency,
110            mode: hit_objects.mode,
111            letterbox_in_breaks: hit_objects.letterbox_in_breaks,
112            special_style: hit_objects.special_style,
113            widescreen_storyboard: hit_objects.widescreen_storyboard,
114            epilepsy_warning: hit_objects.epilepsy_warning,
115            samples_match_playback_rate: hit_objects.samples_match_playback_rate,
116            countdown: hit_objects.countdown,
117            countdown_offset: hit_objects.countdown_offset,
118            hp_drain_rate: hit_objects.hp_drain_rate,
119            circle_size: hit_objects.circle_size,
120            overall_difficulty: hit_objects.overall_difficulty,
121            approach_rate: hit_objects.approach_rate,
122            slider_multiplier: hit_objects.slider_multiplier,
123            slider_tick_rate: hit_objects.slider_tick_rate,
124            background_file: hit_objects.background_file,
125            breaks: hit_objects.breaks,
126            control_points: hit_objects.control_points,
127            hit_objects: hit_objects.hit_objects,
128            ..Self::default()
129        }
130    }
131}
132
133thiserror! {
134    /// All the ways that parsing a `.osu` file into [`HitObjects`] can fail.
135    #[derive(Debug)]
136    pub enum ParseHitObjectsError {
137        #[error("failed to parse difficulty section")]
138        Difficulty(ParseDifficultyError),
139        #[error("failed to parse events section")]
140        Events(#[from] ParseEventsError),
141        #[error("failed to parse hit object type")]
142        HitObjectType(#[from] ParseHitObjectTypeError),
143        #[error("failed to parse hit sound type")]
144        HitSoundType(#[from] ParseHitSoundTypeError),
145        #[error("invalid line")]
146        InvalidLine,
147        #[error("repeat count is way too high")]
148        InvalidRepeatCount(i32),
149        #[error("failed to parse number")]
150        Number(#[from] ParseNumberError),
151        #[error("invalid sample bank")]
152        SampleBankInfo(#[from] ParseSampleBankInfoError),
153        #[error("failed to parse timing points")]
154        TimingPoints(#[from] ParseTimingPointsError),
155        #[error("unknown hit object type")]
156        UnknownHitObjectType(HitObjectType),
157    }
158}
159
160/// The parsing state for [`HitObjects`] in [`DecodeBeatmap`].
161pub struct HitObjectsState {
162    pub last_object: Option<HitObjectType>,
163    pub curve_points: Vec<PathControlPoint>,
164    pub vertices: Vec<PathControlPoint>,
165    pub events: EventsState,
166    pub timing_points: TimingPointsState,
167    pub difficulty: DifficultyState,
168    pub hit_objects: Vec<HitObject>,
169    point_split: Vec<*const str>,
170}
171
172impl HitObjectsState {
173    pub const fn difficulty(&self) -> &Difficulty {
174        &self.difficulty.difficulty
175    }
176
177    const fn first_object(&self) -> bool {
178        self.last_object.is_none()
179    }
180
181    /// Processes the point string of a slider hit object.
182    fn convert_path_str(
183        &mut self,
184        point_str: &str,
185        offset: Pos,
186    ) -> Result<(), ParseHitObjectsError> {
187        let f = |this: &mut Self, point_split: &[&str]| {
188            let mut start_idx = 0;
189            let mut end_idx = 0;
190            let mut first = true;
191
192            while {
193                end_idx += 1;
194
195                end_idx < point_split.len()
196            } {
197                let is_letter = point_split[end_idx]
198                    .chars()
199                    .next()
200                    .ok_or(ParseHitObjectsError::InvalidLine)?
201                    .is_ascii_alphabetic();
202
203                if !is_letter {
204                    continue;
205                }
206
207                let end_point = point_split.get(end_idx + 1).copied();
208                this.convert_points(&point_split[start_idx..end_idx], end_point, first, offset)?;
209
210                start_idx = end_idx;
211                first = false;
212            }
213
214            if end_idx > start_idx {
215                this.convert_points(&point_split[start_idx..end_idx], None, first, offset)?;
216            }
217
218            Ok(())
219        };
220
221        self.point_split(point_str.split('|'), f)
222    }
223
224    /// Process a slice of points and store them in internal buffers.
225    fn convert_points(
226        &mut self,
227        points: &[&str],
228        end_point: Option<&str>,
229        first: bool,
230        offset: Pos,
231    ) -> Result<(), ParseHitObjectsError> {
232        fn read_point(
233            value: &str,
234            start_pos: Pos,
235        ) -> Result<PathControlPoint, ParseHitObjectsError> {
236            let mut v = value
237                .split(':')
238                .map(|s| s.parse_with_limits(f64::from(MAX_COORDINATE_VALUE)));
239
240            let (x, y) = v
241                .next()
242                .zip(v.next())
243                .ok_or(ParseHitObjectsError::InvalidLine)?;
244
245            let pos = Pos::new(x? as i32 as f32, y? as i32 as f32);
246
247            Ok(PathControlPoint::new(pos - start_pos))
248        }
249
250        fn is_linear(p0: Pos, p1: Pos, p2: Pos) -> bool {
251            ((p1.y - p0.y) * (p2.x - p0.x) - (p1.x - p0.x) * (p2.y - p0.y)).abs() < f32::EPSILON
252        }
253
254        let mut path_type = points
255            .first()
256            .copied()
257            .map(PathType::new_from_str)
258            .ok_or(ParseHitObjectsError::InvalidLine)?;
259
260        let read_offset = usize::from(first);
261        let readable_points = points.len() - 1;
262        let end_point_len = usize::from(end_point.is_some());
263
264        self.vertices.clear();
265        self.vertices
266            .reserve(read_offset + readable_points + end_point_len);
267
268        if first {
269            self.vertices.push(PathControlPoint::default());
270        }
271
272        for &point in points.iter().skip(1) {
273            self.vertices.push(read_point(point, offset)?);
274        }
275
276        if let Some(end_point) = end_point {
277            self.vertices.push(read_point(end_point, offset)?);
278        }
279
280        if path_type == PathType::PERFECT_CURVE {
281            if let [a, b, c] = self.vertices.as_slice() {
282                if is_linear(a.pos, b.pos, c.pos) {
283                    path_type = PathType::LINEAR;
284                }
285            } else {
286                path_type = PathType::BEZIER;
287            }
288        }
289
290        self.vertices
291            .first_mut()
292            .ok_or(ParseHitObjectsError::InvalidLine)?
293            .path_type = Some(path_type);
294
295        let mut start_idx = 0;
296        let mut end_idx = 0;
297
298        while {
299            end_idx += 1;
300
301            end_idx < self.vertices.len() - end_point_len
302        } {
303            if self.vertices[end_idx].pos != self.vertices[end_idx - 1].pos {
304                continue;
305            }
306
307            if path_type == PathType::CATMULL && end_idx > 1 {
308                continue;
309            }
310
311            if end_idx == self.vertices.len() - end_point_len - 1 {
312                continue;
313            }
314
315            self.vertices[end_idx - 1].path_type = Some(path_type);
316
317            self.curve_points.extend(&self.vertices[start_idx..end_idx]);
318
319            start_idx = end_idx + 1;
320        }
321
322        if end_idx > start_idx {
323            self.curve_points.extend(&self.vertices[start_idx..end_idx]);
324        }
325
326        Ok(())
327    }
328
329    /// Whether the last object was a spinner.
330    fn last_object_was_spinner(&self) -> bool {
331        self.last_object
332            .is_some_and(|kind| kind.has_flag(HitObjectType::SPINNER))
333    }
334
335    /// Given a `&str` iterator, this method prepares a slice and provides
336    /// that slice to the given function `f`.
337    ///
338    /// Instead of collecting a `&str` iterator into a new vec each time,
339    /// this method re-uses the same buffer to avoid allocations.
340    ///
341    /// It is a safe abstraction around transmuting the `point_split` field.
342    fn point_split<'a, I, F, O>(&mut self, point_split: I, f: F) -> O
343    where
344        I: Iterator<Item = &'a str>,
345        F: FnOnce(&mut Self, &[&'a str]) -> O,
346    {
347        self.point_split.extend(point_split.map(ptr::from_ref));
348        let ptr = self.point_split.as_ptr();
349        let len = self.point_split.len();
350
351        // SAFETY:
352        // - *const str and &str have the same layout.
353        // - `self.point_split` is cleared after every use, ensuring that it
354        //   does not contain any invalid pointers.
355        let point_split = unsafe { slice::from_raw_parts(ptr.cast(), len) };
356        let res = f(self, point_split);
357        self.point_split.clear();
358
359        res
360    }
361
362    fn post_process_breaks(hit_objects: &mut [HitObject], events: &Events) {
363        let mut curr_break = 0;
364        let mut force_new_combo = false;
365
366        for h in hit_objects.iter_mut() {
367            while curr_break < events.breaks.len()
368                && events.breaks[curr_break].end_time < h.start_time
369            {
370                force_new_combo = true;
371                curr_break += 1;
372            }
373
374            match h.kind {
375                HitObjectKind::Circle(ref mut h) => h.new_combo |= force_new_combo,
376                HitObjectKind::Slider(ref mut h) => h.new_combo |= force_new_combo,
377                HitObjectKind::Spinner(ref mut h) => h.new_combo |= force_new_combo,
378                HitObjectKind::Hold(_) => {}
379            }
380
381            force_new_combo = false;
382        }
383    }
384}
385
386impl DecodeState for HitObjectsState {
387    fn create(version: i32) -> Self {
388        Self {
389            last_object: None,
390            curve_points: Vec::new(),
391            vertices: Vec::new(),
392            point_split: Vec::new(),
393            events: EventsState::create(version),
394            timing_points: TimingPointsState::create(version),
395            difficulty: DifficultyState::create(version),
396            hit_objects: Vec::new(),
397        }
398    }
399}
400
401pub(crate) fn get_precision_adjusted_beat_len(
402    slider_velocity: f64,
403    beat_len: f64,
404    mode: GameMode,
405) -> f64 {
406    let slider_velocity_as_beat_len = -100.0 / slider_velocity;
407
408    let bpm_multiplier = if slider_velocity_as_beat_len < 0.0 {
409        match mode {
410            GameMode::Osu | GameMode::Catch => {
411                (-slider_velocity_as_beat_len).clamp(10.0, 10_000.0) / 100.0
412            }
413            GameMode::Taiko | GameMode::Mania => {
414                (-slider_velocity_as_beat_len).clamp(10.0, 1000.0) / 100.0
415            }
416        }
417    } else {
418        1.0
419    };
420
421    beat_len * bpm_multiplier
422}
423
424impl From<HitObjectsState> for HitObjects {
425    fn from(state: HitObjectsState) -> Self {
426        const CONTROL_POINT_LENIENCY: f64 = 5.0;
427
428        let difficulty: Difficulty = state.difficulty.into();
429        let timing_points: TimingPoints = state.timing_points.into();
430        let events = state.events;
431
432        let mut hit_objects = state.hit_objects;
433        hit_objects.sort_by(|a, b| a.start_time.total_cmp(&b.start_time));
434
435        HitObjectsState::post_process_breaks(&mut hit_objects, &events);
436        let mut bufs = CurveBuffers::default();
437
438        for h in hit_objects.iter_mut() {
439            if let HitObjectKind::Slider(ref mut slider) = h.kind {
440                let beat_len = timing_points
441                    .control_points
442                    .timing_point_at(h.start_time)
443                    .map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
444
445                let slider_velocity = timing_points
446                    .control_points
447                    .difficulty_point_at(h.start_time)
448                    .map_or(DifficultyPoint::DEFAULT_SLIDER_VELOCITY, |point| {
449                        point.slider_velocity
450                    });
451
452                slider.velocity = f64::from(BASE_SCORING_DIST) * difficulty.slider_multiplier
453                    / get_precision_adjusted_beat_len(
454                        slider_velocity,
455                        beat_len,
456                        timing_points.mode,
457                    );
458
459                let span_count = f64::from(slider.span_count());
460                let duration = slider.duration_with_bufs(&mut bufs);
461
462                for i in 0..slider.node_samples.len() {
463                    let time =
464                        h.start_time + i as f64 * duration / span_count + CONTROL_POINT_LENIENCY;
465
466                    let node_sample_point = timing_points
467                        .control_points
468                        .sample_point_at(time)
469                        .map_or_else(SamplePoint::default, SamplePoint::clone);
470
471                    for sample in slider.node_samples[i].iter_mut() {
472                        node_sample_point.apply(sample);
473                    }
474                }
475            }
476
477            let end_time = h.end_time_with_bufs(&mut bufs);
478
479            let sample_point = timing_points
480                .control_points
481                .sample_point_at(end_time + CONTROL_POINT_LENIENCY)
482                .map_or_else(SamplePoint::default, SamplePoint::clone);
483
484            for sample in h.samples.iter_mut() {
485                sample_point.apply(sample);
486            }
487        }
488
489        Self {
490            audio_file: timing_points.audio_file,
491            audio_lead_in: timing_points.audio_lead_in,
492            preview_time: timing_points.preview_time,
493            default_sample_bank: timing_points.default_sample_bank,
494            default_sample_volume: timing_points.default_sample_volume,
495            stack_leniency: timing_points.stack_leniency,
496            mode: timing_points.mode,
497            letterbox_in_breaks: timing_points.letterbox_in_breaks,
498            special_style: timing_points.special_style,
499            widescreen_storyboard: timing_points.widescreen_storyboard,
500            epilepsy_warning: timing_points.epilepsy_warning,
501            samples_match_playback_rate: timing_points.samples_match_playback_rate,
502            countdown: timing_points.countdown,
503            countdown_offset: timing_points.countdown_offset,
504            hp_drain_rate: difficulty.hp_drain_rate,
505            circle_size: difficulty.circle_size,
506            overall_difficulty: difficulty.overall_difficulty,
507            approach_rate: difficulty.approach_rate,
508            slider_multiplier: difficulty.slider_multiplier,
509            slider_tick_rate: difficulty.slider_tick_rate,
510            background_file: events.background_file,
511            breaks: events.breaks,
512            control_points: timing_points.control_points,
513            hit_objects,
514        }
515    }
516}
517
518const MAX_COORDINATE_VALUE: i32 = 131_072;
519
520impl DecodeBeatmap for HitObjects {
521    type Error = ParseHitObjectsError;
522    type State = HitObjectsState;
523
524    fn parse_general(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
525        TimingPoints::parse_general(&mut state.timing_points, line)
526            .map_err(ParseHitObjectsError::TimingPoints)
527    }
528
529    fn parse_editor(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
530        Ok(())
531    }
532
533    fn parse_metadata(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
534        Ok(())
535    }
536
537    fn parse_difficulty(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
538        Difficulty::parse_difficulty(&mut state.difficulty, line)
539            .map_err(ParseHitObjectsError::Difficulty)
540    }
541
542    fn parse_events(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
543        Events::parse_events(&mut state.events, line).map_err(ParseHitObjectsError::Events)
544    }
545
546    fn parse_timing_points(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
547        TimingPoints::parse_timing_points(&mut state.timing_points, line)
548            .map_err(ParseHitObjectsError::TimingPoints)
549    }
550
551    fn parse_colors(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
552        Ok(())
553    }
554
555    // It's preferred to keep the code in-sync with osu!lazer without refactoring.
556    #[allow(clippy::too_many_lines)]
557    fn parse_hit_objects(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
558        let mut split = line.trim_comment().split(',');
559
560        let (Some(x), Some(y), Some(start_time), Some(kind), Some(sound_type)) = (
561            split.next(),
562            split.next(),
563            split.next(),
564            split.next(),
565            split.next(),
566        ) else {
567            return Err(ParseHitObjectsError::InvalidLine);
568        };
569
570        let pos = Pos {
571            x: x.parse_with_limits(MAX_COORDINATE_VALUE as f32)? as i32 as f32,
572            y: y.parse_with_limits(MAX_COORDINATE_VALUE as f32)? as i32 as f32,
573        };
574
575        let start_time_raw = f64::parse(start_time)?;
576        let start_time = start_time_raw;
577        let mut hit_object_type: HitObjectType = kind.parse()?;
578
579        let combo_offset = (hit_object_type & HitObjectType::COMBO_OFFSET) >> 4;
580        hit_object_type &= !HitObjectType::COMBO_OFFSET;
581
582        let new_combo = hit_object_type.has_flag(HitObjectType::NEW_COMBO);
583        hit_object_type &= !HitObjectType::NEW_COMBO;
584
585        let sound_type: HitSoundType = sound_type.parse()?;
586        let mut bank_info = SampleBankInfo::default();
587
588        let kind = if hit_object_type.has_flag(HitObjectType::CIRCLE) {
589            if let Some(s) = split.next() {
590                bank_info.read_custom_sample_banks(s.split(':'), false)?;
591            }
592
593            let circle = HitObjectCircle {
594                pos,
595                new_combo: state.first_object() || state.last_object_was_spinner() || new_combo,
596                combo_offset: if new_combo { combo_offset } else { 0 },
597            };
598
599            HitObjectKind::Circle(circle)
600        } else if hit_object_type.has_flag(HitObjectType::SLIDER) {
601            let (point_str, repeat_count) = split
602                .next()
603                .zip(split.next())
604                .ok_or(ParseHitObjectsError::InvalidLine)?;
605
606            let mut len = None;
607
608            let mut repeat_count = repeat_count.parse_num::<i32>()?;
609
610            if repeat_count > 9000 {
611                return Err(ParseHitObjectsError::InvalidRepeatCount(repeat_count));
612            }
613
614            repeat_count = cmp::max(0, repeat_count - 1);
615
616            if let Some(next) = split.next() {
617                let new_len = next
618                    .parse_with_limits(f64::from(MAX_COORDINATE_VALUE))?
619                    .max(0.0);
620
621                if new_len.abs() >= f64::EPSILON {
622                    len = Some(new_len);
623                }
624            }
625
626            let next_8 = split.next();
627            let next_9 = split.next();
628
629            if let Some(next) = split.next() {
630                bank_info.read_custom_sample_banks(next.split(':'), true)?;
631            }
632
633            let nodes = repeat_count as usize + 2;
634
635            let mut node_bank_infos = vec![bank_info.clone(); nodes];
636
637            if let Some(next) = next_9.filter(|s| !s.is_empty()) {
638                for (bank_info, set) in node_bank_infos.iter_mut().zip(next.split('|')) {
639                    bank_info.read_custom_sample_banks(set.split(':'), false)?;
640                }
641            }
642
643            let mut node_sound_types = vec![sound_type; nodes];
644
645            if let Some(next) = next_8.filter(|s| !s.is_empty()) {
646                for (sound_type, s) in node_sound_types.iter_mut().zip(next.split('|')) {
647                    *sound_type = s.parse().unwrap_or_default();
648                }
649            }
650
651            let node_samples: Vec<_> = node_bank_infos
652                .into_iter()
653                .zip(node_sound_types)
654                .map(|(bank_info, sound_type)| bank_info.convert_sound_type(sound_type))
655                .collect();
656
657            state.convert_path_str(point_str, pos)?;
658            let mut control_points = Vec::with_capacity(state.curve_points.len());
659            control_points.append(&mut state.curve_points);
660
661            let slider = HitObjectSlider {
662                pos,
663                new_combo: state.first_object() || state.last_object_was_spinner() || new_combo,
664                combo_offset: if new_combo { combo_offset } else { 0 },
665                path: SliderPath::new(state.timing_points.mode(), control_points, len),
666                node_samples,
667                repeat_count,
668                velocity: 1.0,
669            };
670
671            HitObjectKind::Slider(slider)
672        } else if hit_object_type.has_flag(HitObjectType::SPINNER) {
673            let duration = split
674                .next()
675                .ok_or(ParseHitObjectsError::InvalidLine)?
676                .parse_num::<f64>()?;
677
678            let duration = (duration - start_time).max(0.0);
679
680            if let Some(s) = split.next() {
681                bank_info.read_custom_sample_banks(s.split(':'), false)?;
682            }
683
684            let spinner = HitObjectSpinner {
685                pos: Pos::new(512.0 / 2.0, 384.0 / 2.0),
686                duration,
687                new_combo,
688            };
689
690            HitObjectKind::Spinner(spinner)
691        } else if hit_object_type.has_flag(HitObjectType::HOLD) {
692            let mut end_time = start_time.max(start_time_raw);
693
694            if let Some(s) = split.next().filter(|s| !s.is_empty()) {
695                let mut ss = s.split(':');
696
697                let new_end_time = ss
698                    .next()
699                    .ok_or(ParseHitObjectsError::InvalidLine)?
700                    .parse_num::<f64>()?;
701
702                end_time = start_time.max(new_end_time);
703
704                bank_info.read_custom_sample_banks(ss, false)?;
705            }
706
707            let hold = HitObjectHold {
708                pos_x: pos.x,
709                duration: end_time - start_time,
710            };
711
712            HitObjectKind::Hold(hold)
713        } else {
714            return Err(ParseHitObjectsError::UnknownHitObjectType(hit_object_type));
715        };
716
717        let result = HitObject {
718            start_time,
719            kind,
720            samples: bank_info.convert_sound_type(sound_type),
721        };
722
723        state.last_object = Some(hit_object_type);
724        state.hit_objects.push(result);
725
726        Ok(())
727    }
728
729    fn parse_variables(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
730        Ok(())
731    }
732
733    fn parse_catch_the_beat(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
734        Ok(())
735    }
736
737    fn parse_mania(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
738        Ok(())
739    }
740}