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#[derive(Clone, Debug, PartialEq)]
30pub struct HitObjects {
31 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 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 pub background_file: String,
57 pub breaks: Vec<BreakPeriod>,
58
59 pub control_points: ControlPoints,
61
62 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 #[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
160pub 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 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 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 fn last_object_was_spinner(&self) -> bool {
331 self.last_object
332 .is_some_and(|kind| kind.has_flag(HitObjectType::SPINNER))
333 }
334
335 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 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 #[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}