1use std::{
2 borrow::Cow,
3 ops::{Add, Sub},
4};
5
6use thiserror::Error;
7
8use crate::{
9 BeatmapSection, Context, EventSampleSet, EventTrigger, FullHitSound, HitObject, HitObjectFlags,
10 HitObjectKind, HitSound, SampleSet, SliderKind, SoundType, SoundTypes, Span, StaticCow, Time,
11 TimingPoint, TimingPointFlags,
12};
13
14pub trait ParseField<'a>: Sized {
15 fn parse_field(
16 name: impl Into<Cow<'static, str>>,
17 ctx: &Context,
18 line: impl StaticCow<'a>,
19 ) -> Result<Self, ParseError>;
20}
21
22macro_rules! impl_parse_field {
23 ($($T:ty),*) => {
24 $(impl<'a> ParseField<'a> for $T {
25 fn parse_field(
26 name: impl Into<Cow<'static, str>>,
27 _ctx: &Context,
28 line: impl StaticCow<'a>,
29 ) -> Result<Self, ParseError> {
30 line.as_ref().parse().map_err(ParseError::curry(name, line.span()))
31 }
32 })*
33 };
34}
35
36impl_parse_field!(u8, u16, u32, u64, usize);
37impl_parse_field!(i8, i16, i32, i64, isize);
38impl_parse_field!(f32, f64);
39impl_parse_field!(TimingPointFlags);
40
41impl<'a, T: ParseField<'a>> BeatmapSection<'a> for Vec<T> {
42 fn consume_line(
43 &mut self,
44 ctx: &Context,
45 line: impl StaticCow<'a>,
46 ) -> Result<Option<crate::Section>, ParseError> {
47 self.push(ParseField::parse_field("", ctx, line)?);
48 Ok(None)
49 }
50}
51
52impl<'a, T: Copy + Clone + From<i8> + Add<T, Output = T> + Sub<T, Output = T> + ParseField<'a>>
53 ParseField<'a> for Time<T>
54{
55 fn parse_field(
56 name: impl Into<Cow<'static, str>>,
57 ctx: &Context,
58 line: impl StaticCow<'a>,
59 ) -> Result<Self, ParseError> {
60 Ok(Self::new(
61 ParseField::parse_field(name, ctx, line)?,
62 ctx.version,
63 ))
64 }
65}
66
67impl<'a> ParseField<'a> for Vec<i32> {
68 fn parse_field(
69 name: impl Into<Cow<'static, str>>,
70 ctx: &Context,
71 line: impl StaticCow<'a>,
72 ) -> Result<Self, ParseError> {
73 let mut ret = vec![];
74 let name = name.into();
75 for val in line.split(',') {
76 if !val.as_ref().is_empty() {
77 ret.push(ParseField::parse_field(name.clone(), ctx, val)?);
78 }
79 }
80 Ok(ret)
81 }
82}
83
84impl<'a> ParseField<'a> for bool {
85 fn parse_field(
86 name: impl Into<Cow<'static, str>>,
87 _ctx: &Context,
88 line: impl StaticCow<'a>,
89 ) -> Result<Self, ParseError> {
90 match line.as_ref().bytes().next() {
91 Some(b'1') => Ok(true),
92 Some(_) => Ok(false),
93 None => Err(ParseError::curry(name, line.span())(NoBoolValue)),
94 }
95 }
96}
97
98impl<'a> ParseField<'a> for TimingPoint {
99 fn parse_field(
100 _name: impl Into<Cow<'static, str>>,
101 ctx: &Context,
102 line: impl StaticCow<'a>,
103 ) -> Result<Self, ParseError> {
104 let mut end_span = line.span();
105 end_span.start = end_span.end;
106 let mut s = line.split(',');
107 let offset = s
108 .next()
109 .ok_or(InvalidTimingPoint)
110 .map_err(ParseError::curry("timing point offset", end_span))?;
111 let beat_length = s
112 .next()
113 .ok_or(InvalidTimingPoint)
114 .map_err(ParseError::curry("timing point beat length", end_span))?;
115 let mut ret = TimingPoint {
116 offset: ParseField::parse_field("timing point offset", ctx, offset)?,
117 beat_length: ParseField::parse_field("timing point beat length", ctx, beat_length)?,
118 time_signature: 4,
119 sample_set: None,
120 custom_sample_index: 0,
121 sample_volume: 100,
122 changes_timing: true,
123 flags: TimingPointFlags::empty(),
124 };
125 let Some(time_signature) = s.next() else {
126 return Ok(ret);
127 };
128 if !time_signature.as_ref().starts_with('0') {
129 ret.time_signature =
130 ParseField::parse_field("timing point time signature", ctx, time_signature)?;
131 }
132 let Some(sample_set) = s.next() else {
133 return Ok(ret);
134 };
135 ret.sample_set = Some(
136 i32::parse_field("timing point sample set", ctx, sample_set)?
137 .try_into()
138 .map_err(ParseError::curry(
139 "timing point sample set",
140 sample_set.span(),
141 ))?,
142 );
143 let Some(custom_sample_index) = s.next() else {
144 return Ok(ret);
145 };
146 ret.custom_sample_index =
147 ParseField::parse_field("timing point sample index", ctx, custom_sample_index)?;
148 let Some(sample_volume) = s.next() else {
149 return Ok(ret);
150 };
151 ret.sample_volume =
152 ParseField::parse_field("timing point sample volume", ctx, sample_volume)?;
153 let Some(changes_timing) = s.next() else {
154 return Ok(ret);
155 };
156 ret.changes_timing = ParseField::parse_field("timing point type", ctx, changes_timing)?;
157 let Some(flags) = s.next() else {
158 return Ok(ret);
159 };
160 ret.flags = ParseField::parse_field("timing point flags", ctx, flags)?;
161 Ok(ret)
162 }
163}
164
165impl<'a> ParseField<'a> for HitObject<'a> {
166 fn parse_field(
167 _name: impl Into<Cow<'static, str>>,
168 ctx: &Context,
169 line: impl StaticCow<'a>,
170 ) -> Result<Self, ParseError> {
171 let mut end_span = line.span();
172 end_span.start = end_span.end;
173 let mut s = line.split(',');
174 let x = s
175 .next()
176 .ok_or(InvalidHitObject)
177 .map_err(ParseError::curry("hit object x", end_span))?;
178 let x = f64::parse_field("hit object x", ctx, x)?;
179 let x = x as i32;
180 let y = s
181 .next()
182 .ok_or(InvalidHitObject)
183 .map_err(ParseError::curry("hit object y", end_span))?;
184 let y = f64::parse_field("hit object y", ctx, y)?;
185 let y = y as i32;
186 let time = s
187 .next()
188 .ok_or(InvalidHitObject)
189 .map_err(ParseError::curry("hit object time", end_span))?;
190 let time = f64::parse_field("hit object time", ctx, time)?;
191 let time = time as i32;
192 let kind = s
193 .next()
194 .ok_or(InvalidHitObject)
195 .map_err(ParseError::curry("hit object type", end_span))?;
196 let type_span = kind.span();
197 let kind = i32::parse_field("hit object type", ctx, kind)?;
198 let kind = HitObjectFlags::from_bits_retain(kind);
199 let sound = s
200 .next()
201 .ok_or(InvalidHitObject)
202 .map_err(ParseError::curry("hit object sound", end_span))?;
203 let sound = i32::parse_field("hit object sound", ctx, sound)?;
204 let sound = SoundTypes::from_bits_retain(sound);
205 let combo_start = kind.contains(HitObjectFlags::COMBO_START);
206 let combo_colour_skip = (kind & HitObjectFlags::COMBO_COLOUR_OFFSET_MASK).bits() >> 4;
207 let (kind, extra) = if kind.contains(HitObjectFlags::CIRCLE) {
208 (HitObjectKind::Circle, s.next())
209 } else if kind.contains(HitObjectFlags::SLIDER) {
210 let points = s
211 .next()
212 .ok_or(InvalidHitObject)
213 .map_err(ParseError::curry("slider points", end_span))?;
214 let mut kind = SliderKind::Catmull;
215 let mut curve_points = Vec::new();
216 for point in points.split('|') {
217 if point.as_ref().len() == 1 {
218 kind = SliderKind::try_from(point.as_ref().chars().next().unwrap())
219 .map_err(ParseError::curry("slider type", point.span()))?;
220 continue;
221 }
222 let (x, y) = point
223 .split_once(':')
224 .ok_or(InvalidHitObject)
225 .map_err(ParseError::curry("slider point coordinates", point.span()))?;
226 let x = f64::parse_field("slider point x", ctx, x)?;
227 let x = x as i32;
228 let y = f64::parse_field("slider point x", ctx, y)?;
229 let y = y as i32;
230 curve_points.push((x, y));
231 }
232 let slide_count = s
233 .next()
234 .ok_or(InvalidHitObject)
235 .map_err(ParseError::curry("slider slide count", end_span))?;
236 let slide_count = i32::parse_field("slider slide count", ctx, slide_count)?;
237 let length = if let Some(length) = s.next() {
238 ParseField::parse_field("slider length", ctx, length)?
239 } else {
240 0.
241 };
242 let mut edge_sounds = if let Some(edge_sounds) = s.next() {
243 let mut sounds = vec![];
244 if !edge_sounds.as_ref().is_empty() {
245 for sound in edge_sounds.split('|') {
246 sounds.push(HitSound {
247 sounds: SoundTypes::from_bits_retain(ParseField::parse_field(
248 "slider edge sound",
249 ctx,
250 sound,
251 )?),
252 addition_set: SampleSet::None,
253 sample_set: SampleSet::None,
254 });
255 }
256 }
257 sounds
258 } else {
259 vec![]
260 };
261 if let Some(edge_samples) = s.next() {
262 if !edge_samples.as_ref().is_empty() {
263 for (i, sound) in edge_samples.split('|').enumerate() {
264 let (sample, addition) = sound
265 .split_once(':')
266 .ok_or(InvalidHitObject)
267 .map_err(ParseError::curry("slider edge sample sets", sound.span()))?;
268 let sample = SampleSet::try_from(i32::parse_field(
269 "slider edge sample set",
270 ctx,
271 sample,
272 )?)
273 .map_err(ParseError::curry("slider edge sample set", sample.span()))?;
274 let addition = SampleSet::try_from(i32::parse_field(
275 "slider edge sample addition set",
276 ctx,
277 addition,
278 )?)
279 .map_err(ParseError::curry(
280 "slider edge sample addition set",
281 addition.span(),
282 ))?;
283 if let Some(val) = edge_sounds.get_mut(i) {
284 val.sample_set = sample;
285 val.addition_set = addition;
286 } else {
287 edge_sounds.push(HitSound {
288 sounds: SoundTypes::empty(),
289 sample_set: sample,
290 addition_set: addition,
291 });
292 }
293 }
294 }
295 }
296 (
297 HitObjectKind::Slider {
298 kind,
299 curve_points,
300 length,
301 edge_sounds,
302 slide_count,
303 },
304 s.next(),
305 )
306 } else if kind.contains(HitObjectFlags::SPINNER) {
307 let end_time = s
308 .next()
309 .ok_or(InvalidHitObject)
310 .map_err(ParseError::curry("spinner end time", end_span))?;
311 let end_time = i32::parse_field("spinner end time", ctx, end_time)?;
312 (HitObjectKind::Spinner { end_time }, s.next())
313 } else if kind.contains(HitObjectFlags::HOLD_NOTE) {
314 let end_time_and_extra = s
315 .next()
316 .ok_or(InvalidHitObject)
317 .map_err(ParseError::curry("hold note end time", end_span))?;
318 let (end_time, extra) = end_time_and_extra
319 .split_once(':')
320 .map(|(a, b)| (a, Some(b)))
321 .unwrap_or((end_time_and_extra, None));
322 let end_time = i32::parse_field("hold note end time", ctx, end_time)?;
323 (HitObjectKind::HoldNote { end_time }, extra)
324 } else {
325 return Err(ParseError::curry("hit object type", type_span)(
326 InvalidHitObject,
327 ));
328 };
329 let mut hit_sound = FullHitSound {
330 hit_sound: HitSound {
331 sounds: sound,
332 sample_set: SampleSet::None,
333 addition_set: SampleSet::None,
334 },
335 custom_sample_index: 0,
336 volume: 0,
337 sample_file: "".into(),
338 };
339 if let Some(extra) = extra.filter(|x| !x.as_ref().is_empty()) {
340 let mut s = extra.split(':');
341 let sample = s
342 .next()
343 .ok_or(InvalidHitObject)
344 .map_err(ParseError::curry("hit object sample set", end_span))?;
345 hit_sound.hit_sound.sample_set =
346 SampleSet::try_from(i32::parse_field("hit object sample set", ctx, sample)?)
347 .map_err(ParseError::curry("hit object sample set", sample.span()))?;
348 let addition = s.next().ok_or(InvalidHitObject).map_err(ParseError::curry(
349 "hit object sample addition set",
350 end_span,
351 ))?;
352 hit_sound.hit_sound.addition_set = SampleSet::try_from(i32::parse_field(
353 "hit object sample addition set",
354 ctx,
355 addition,
356 )?)
357 .map_err(ParseError::curry(
358 "hit object sample addition set",
359 addition.span(),
360 ))?;
361 if let Some(custom) = s.next() {
362 hit_sound.custom_sample_index =
363 i32::parse_field("hit object custom sample index", ctx, custom)?;
364 }
365 if let Some(volume) = s.next() {
366 let volume = i32::parse_field("hit object sample volume", ctx, volume)?;
367 hit_sound.volume = volume;
368 }
369 if let Some(sample_file) = s.next() {
370 hit_sound.sample_file = sample_file.into_cow();
371 }
372 }
373 Ok(Self {
374 x,
375 y,
376 time,
377 combo_start,
378 combo_colour_skip,
379 hit_sound,
380 kind,
381 })
382 }
383}
384
385impl<'a> ParseField<'a> for EventTrigger {
386 fn parse_field(
387 name: impl Into<Cow<'static, str>>,
388 _ctx: &Context,
389 line: impl StaticCow<'a>,
390 ) -> Result<Self, ParseError> {
391 match line.as_ref() {
392 "Passing" => Ok(Self::Passing),
393 "Failing" => Ok(Self::Failing),
394 "HitObjectHit" => Ok(Self::HitObjectHit),
395 _ => {
396 if let Some(s) = line.as_ref().strip_prefix("HitSound") {
397 let (set, s) = if let Some(s) = s.strip_prefix("All") {
398 (Some(EventSampleSet::All), s)
399 } else if let Some(s) = s.strip_prefix("Normal") {
400 (Some(EventSampleSet::Normal), s)
401 } else if let Some(s) = s.strip_prefix("Soft") {
402 (Some(EventSampleSet::Soft), s)
403 } else if let Some(s) = s.strip_prefix("Drum") {
404 (Some(EventSampleSet::Drum), s)
405 } else {
406 (None, s)
407 };
408 let (sample_set, addition_set, s) = if let Some(set) = set {
409 let (add, s) = if let Some(s) = s.strip_prefix("All") {
410 (Some(EventSampleSet::All), s)
411 } else if let Some(s) = s.strip_prefix("Normal") {
412 (Some(EventSampleSet::Normal), s)
413 } else if let Some(s) = s.strip_prefix("Soft") {
414 (Some(EventSampleSet::Soft), s)
415 } else if let Some(s) = s.strip_prefix("Drum") {
416 (Some(EventSampleSet::Drum), s)
417 } else {
418 (None, s)
419 };
420 (Some(set), add, s)
421 } else {
422 (None, None, s)
423 };
424 let (sound, s) = if let Some(s) = s.strip_prefix("Whistle") {
425 (Some(SoundType::Whistle), s)
426 } else if let Some(s) = s.strip_prefix("Finish") {
427 (Some(SoundType::Finish), s)
428 } else if let Some(s) = s.strip_prefix("Clap") {
429 (Some(SoundType::Clap), s)
430 } else {
431 (None, s)
432 };
433 let custom_sample_index = s.parse().ok();
434 Ok(Self::HitSound {
435 sample_set,
436 addition_set,
437 sound,
438 custom_sample_index,
439 })
440 } else {
441 Err(ParseError::curry(name, line.span())(InvalidEventCommand))
442 }
443 }
444 }
445 }
446}
447
448impl std::fmt::Display for EventTrigger {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 match self {
451 Self::Passing => f.write_str("Passing"),
452 Self::Failing => f.write_str("Failing"),
453 Self::HitObjectHit => f.write_str("HitObjectHit"),
454 Self::HitSound {
455 sample_set,
456 addition_set,
457 sound,
458 custom_sample_index,
459 } => {
460 let sample_set = if addition_set.is_some() {
461 Some(sample_set.unwrap_or_default())
462 } else {
463 *sample_set
464 };
465 f.write_str("HitSound")?;
466 for set in [sample_set, *addition_set].into_iter().flatten() {
467 write!(f, "{set}")?;
468 }
469 if let Some(sound) = sound {
470 write!(f, "{sound}")?;
471 }
472 if let Some(idx) = custom_sample_index {
473 write!(f, "{idx}")?;
474 }
475 Ok(())
476 }
477 }
478 }
479}
480
481impl<'a> ParseField<'a> for Cow<'a, str> {
482 fn parse_field(
483 _name: impl Into<Cow<'static, str>>,
484 _ctx: &Context,
485 line: impl StaticCow<'a>,
486 ) -> Result<Self, ParseError> {
487 Ok(line.into_cow())
488 }
489}
490
491#[derive(Debug, Error)]
492pub struct EnumParseError {
493 pub valid_variants: &'static [&'static str],
494}
495
496impl std::fmt::Display for EnumParseError {
497 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498 f.write_str("unexpected enum value (valid values: ")?;
499 let mut first = true;
500 for x in self.valid_variants {
501 if first {
502 first = false;
503 } else {
504 f.write_str(", ")?;
505 }
506 f.write_str(x)?;
507 }
508 f.write_str(")")
509 }
510}
511
512#[derive(Debug, Error)]
513pub struct IntEnumParseError {
514 pub variant: i32,
515 pub valid_variants: &'static [i32],
516}
517
518impl std::fmt::Display for IntEnumParseError {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 write!(f, "unexpected enum value {} (valid values: ", self.variant)?;
521 let mut first = true;
522 for x in self.valid_variants {
523 if first {
524 first = false;
525 } else {
526 f.write_str(", ")?;
527 }
528 x.fmt(f)?;
529 }
530 f.write_str(")")
531 }
532}
533
534#[derive(Debug, Error)]
535pub struct CharEnumParseError {
536 pub variant: char,
537 pub valid_variants: &'static [char],
538}
539
540impl std::fmt::Display for CharEnumParseError {
541 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542 write!(f, "unexpected enum value {} (valid values: ", self.variant)?;
543 let mut first = true;
544 for x in self.valid_variants {
545 if first {
546 first = false;
547 } else {
548 f.write_str(", ")?;
549 }
550 x.fmt(f)?;
551 }
552 f.write_str(")")
553 }
554}
555
556#[derive(Debug, Error)]
557pub struct InvalidRecordField;
558
559impl std::fmt::Display for InvalidRecordField {
560 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
561 write!(f, "invalid record field")
562 }
563}
564
565#[derive(Debug, Error)]
566pub struct RecordParseError {
567 pub valid_fields: &'static [&'static str],
568}
569
570impl std::fmt::Display for RecordParseError {
571 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572 f.write_str("unexpected field (valid fields: ")?;
573 let mut first = true;
574 for x in self.valid_fields {
575 if first {
576 first = false;
577 } else {
578 f.write_str(", ")?;
579 }
580 f.write_str(x)?;
581 }
582 f.write_str(")")
583 }
584}
585
586#[derive(Debug, Error)]
587pub struct NoBoolValue;
588
589impl std::fmt::Display for NoBoolValue {
590 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
591 f.write_str("missing boolean value")
592 }
593}
594
595#[derive(Debug, Error)]
596pub struct InvalidColour;
597
598impl std::fmt::Display for InvalidColour {
599 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600 f.write_str("invalid colour")
601 }
602}
603
604#[derive(Debug, Error)]
605pub struct InvalidTimingPoint;
606
607impl std::fmt::Display for InvalidTimingPoint {
608 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609 f.write_str("invalid timing point")
610 }
611}
612
613#[derive(Debug, Error)]
614pub struct InvalidHitObject;
615
616impl std::fmt::Display for InvalidHitObject {
617 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618 f.write_str("invalid hit object")
619 }
620}
621
622#[derive(Debug, Error)]
623pub struct InvalidEvent;
624
625impl std::fmt::Display for InvalidEvent {
626 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
627 f.write_str("invalid event")
628 }
629}
630
631#[derive(Debug, Error)]
632pub struct InvalidEventCommand;
633
634impl std::fmt::Display for InvalidEventCommand {
635 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636 f.write_str("invalid event command")
637 }
638}
639
640#[derive(Debug, Error)]
641pub enum ParseErrorReason {
642 #[error("{0}")]
643 Bool(
644 #[from]
645 #[source]
646 NoBoolValue,
647 ),
648 #[error("{0}")]
649 InvalidColour(
650 #[from]
651 #[source]
652 InvalidColour,
653 ),
654 #[error("{0}")]
655 InvalidTimingPoint(
656 #[from]
657 #[source]
658 InvalidTimingPoint,
659 ),
660 #[error("{0}")]
661 InvalidHitObject(
662 #[from]
663 #[source]
664 InvalidHitObject,
665 ),
666 #[error("{0}")]
667 InvalidEvent(
668 #[from]
669 #[source]
670 InvalidEvent,
671 ),
672 #[error("{0}")]
673 InvalidEventCommand(
674 #[from]
675 #[source]
676 InvalidEventCommand,
677 ),
678 #[error("{0}")]
679 Int(
680 #[from]
681 #[source]
682 std::num::ParseIntError,
683 ),
684 #[error("{0}")]
685 Float(
686 #[from]
687 #[source]
688 std::num::ParseFloatError,
689 ),
690 #[error("{0}")]
691 Enum(
692 #[from]
693 #[source]
694 EnumParseError,
695 ),
696 #[error("{0}")]
697 Record(
698 #[from]
699 #[source]
700 RecordParseError,
701 ),
702 #[error("{0}")]
703 InvalidRecordField(
704 #[from]
705 #[source]
706 InvalidRecordField,
707 ),
708 #[error("{0}")]
709 IntEnum(
710 #[from]
711 #[source]
712 IntEnumParseError,
713 ),
714 #[error("{0}")]
715 CharEnum(
716 #[from]
717 #[source]
718 CharEnumParseError,
719 ),
720}
721
722impl From<std::convert::Infallible> for ParseErrorReason {
723 fn from(value: std::convert::Infallible) -> Self {
724 match value {}
725 }
726}
727
728#[derive(Debug, Error)]
729pub struct ParseError {
730 pub field: Cow<'static, str>,
731 pub span: Span,
732 #[source]
733 pub reason: ParseErrorReason,
734}
735
736impl ParseError {
737 pub fn curry<E: Into<ParseErrorReason>>(
738 field: impl Into<Cow<'static, str>>,
739 span: Span,
740 ) -> impl FnOnce(E) -> Self {
741 move |reason| Self {
742 field: field.into(),
743 span,
744 reason: reason.into(),
745 }
746 }
747}
748
749impl std::fmt::Display for ParseError {
750 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
751 f.write_str("failed to parse ")?;
752 f.write_str(&self.field)?;
753 f.write_str(": ")?;
754 self.reason.fmt(f)
755 }
756}