osu_file_parser/osu_file/events/normal_event/
mod.rs

1use nom::{
2    branch::alt,
3    bytes::complete::tag,
4    combinator::eof,
5    error::context,
6    sequence::{preceded, tuple},
7    Parser,
8};
9
10use crate::{
11    osu_file::{FilePath, Integer, Position, Version, VersionedFromStr, VersionedToString},
12    parsers::{
13        comma, comma_field, comma_field_type, comma_field_versioned_type, consume_rest_type,
14        consume_rest_versioned_type,
15    },
16    Decimal,
17};
18
19pub use error::*;
20
21use self::types::{LayerLegacy, OriginTypeLegacy};
22
23use super::{storyboard::cmds::Command, EventWithCommands, Volume, OLD_VERSION_TIME_OFFSET};
24
25pub mod error;
26mod parsers;
27pub mod types;
28use parsers::*;
29
30fn position_str(position: &Option<Position>) -> String {
31    match position {
32        Some(position) => format!(",{},{}", position.x, position.y),
33        None => String::new(),
34    }
35}
36
37fn time_to_string(time: Integer, version: Version) -> String {
38    let time = if (3..=4).contains(&version) {
39        time - OLD_VERSION_TIME_OFFSET
40    } else {
41        time
42    };
43
44    time.to_string()
45}
46
47#[derive(Clone, Debug, Hash, PartialEq, Eq)]
48pub struct Background {
49    pub start_time: Integer,
50    pub file_name: FilePath,
51    pub position: Option<Position>,
52    pub commands: Vec<Command>,
53}
54
55pub const BACKGROUND_HEADER: &str = "0";
56
57impl VersionedFromStr for Background {
58    type Err = ParseBackgroundError;
59
60    fn from_str(s: &str, _: Version) -> std::result::Result<Option<Self>, Self::Err> {
61        let (_, (start_time, (filename, position))) = preceded(
62            tuple((
63                context(
64                    ParseBackgroundError::WrongEventType.into(),
65                    tag(BACKGROUND_HEADER),
66                ),
67                context(ParseBackgroundError::MissingStartTime.into(), comma()),
68            )),
69            tuple((
70                context(
71                    ParseBackgroundError::InvalidStartTime.into(),
72                    comma_field_type(),
73                ),
74                preceded(
75                    context(ParseBackgroundError::MissingFileName.into(), comma()),
76                    file_name_and_position(
77                        ParseBackgroundError::MissingX.into(),
78                        ParseBackgroundError::InvalidX.into(),
79                        ParseBackgroundError::MissingY.into(),
80                        ParseBackgroundError::InvalidY.into(),
81                    ),
82                ),
83            )),
84        )(s)?;
85
86        Ok(Some(Background {
87            start_time,
88            file_name: filename,
89            position,
90            commands: Vec::new(),
91        }))
92    }
93}
94
95impl VersionedToString for Background {
96    fn to_string(&self, version: Version) -> Option<String> {
97        self.to_string_variables(version, &[])
98    }
99}
100
101impl EventWithCommands for Background {
102    fn commands(&self) -> &[Command] {
103        &self.commands
104    }
105
106    fn commands_mut(&mut self) -> &mut Vec<Command> {
107        &mut self.commands
108    }
109
110    fn to_string_cmd(&self, version: Version) -> Option<String> {
111        Some(format!(
112            "{BACKGROUND_HEADER},{},{}{}",
113            self.start_time,
114            self.file_name.to_string(version).unwrap(),
115            position_str(&self.position),
116        ))
117    }
118}
119
120#[derive(Clone, Debug, Hash, PartialEq, Eq)]
121pub struct Video {
122    pub start_time: Integer,
123    pub file_name: FilePath,
124    pub position: Option<Position>,
125    pub commands: Vec<Command>,
126    short_hand: bool,
127}
128
129impl Video {
130    pub fn new(start_time: Integer, file_name: FilePath, position: Option<Position>) -> Self {
131        Self {
132            commands: Vec::new(),
133            start_time,
134            file_name,
135            position,
136            short_hand: true,
137        }
138    }
139}
140
141pub const VIDEO_HEADER: &str = "1";
142pub const VIDEO_HEADER_LONG: &str = "Video";
143
144impl VersionedFromStr for Video {
145    type Err = ParseVideoError;
146
147    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
148        let (_, (short_hand, start_time, (file_name, position))) = tuple((
149            alt((
150                tag(VIDEO_HEADER).map(|_| true),
151                context(
152                    ParseVideoError::WrongEventType.into(),
153                    tag(VIDEO_HEADER_LONG).map(|_| false),
154                ),
155            )),
156            preceded(
157                context(ParseVideoError::MissingStartTime.into(), comma()),
158                start_time_offset(ParseVideoError::InvalidStartTime.into(), version),
159            ),
160            preceded(
161                context(ParseVideoError::MissingFileName.into(), comma()),
162                file_name_and_position(
163                    ParseVideoError::MissingX.into(),
164                    ParseVideoError::InvalidX.into(),
165                    ParseVideoError::MissingY.into(),
166                    ParseVideoError::InvalidY.into(),
167                ),
168            ),
169        ))(s)?;
170
171        Ok(Some(Video {
172            commands: Vec::new(),
173            start_time,
174            file_name,
175            position,
176            short_hand,
177        }))
178    }
179}
180
181impl VersionedToString for Video {
182    fn to_string(&self, version: Version) -> Option<String> {
183        self.to_string_variables(version, &[])
184    }
185}
186
187impl EventWithCommands for Video {
188    fn commands(&self) -> &[Command] {
189        &self.commands
190    }
191
192    fn commands_mut(&mut self) -> &mut Vec<Command> {
193        &mut self.commands
194    }
195
196    fn to_string_cmd(&self, version: Version) -> Option<String> {
197        Some(format!(
198            "{},{},{}{}",
199            if self.short_hand {
200                VIDEO_HEADER
201            } else {
202                VIDEO_HEADER_LONG
203            },
204            time_to_string(self.start_time, version),
205            self.file_name.to_string(version).unwrap(),
206            position_str(&self.position)
207        ))
208    }
209}
210
211#[derive(Clone, Debug, Hash, PartialEq, Eq)]
212pub struct Break {
213    pub start_time: Integer,
214    pub end_time: Integer,
215    short_hand: bool,
216}
217
218impl Break {
219    pub fn new(start_time: Integer, end_time: Integer) -> Self {
220        Self {
221            start_time,
222            end_time,
223            short_hand: true,
224        }
225    }
226}
227
228pub const BREAK_HEADER: &str = "2";
229pub const BREAK_HEADER_LONG: &str = "Break";
230
231impl VersionedFromStr for Break {
232    type Err = ParseBreakError;
233
234    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
235        let (_, (short_hand, start_time, end_time)) = tuple((
236            alt((
237                tag(BREAK_HEADER).map(|_| true),
238                context(
239                    ParseBreakError::WrongEventType.into(),
240                    tag(BREAK_HEADER_LONG).map(|_| false),
241                ),
242            )),
243            preceded(
244                context(ParseBreakError::MissingStartTime.into(), comma()),
245                start_time_offset(ParseBreakError::InvalidStartTime.into(), version),
246            ),
247            preceded(
248                context(ParseBreakError::MissingEndTime.into(), comma()),
249                end_time(ParseBreakError::InvalidEndTime.into(), version),
250            ),
251        ))(s)?;
252
253        Ok(Some(Break {
254            start_time,
255            end_time,
256            short_hand,
257        }))
258    }
259}
260
261impl VersionedToString for Break {
262    fn to_string(&self, version: Version) -> Option<String> {
263        Some(format!(
264            "{},{},{}",
265            if self.short_hand {
266                BREAK_HEADER
267            } else {
268                BREAK_HEADER_LONG
269            },
270            time_to_string(self.start_time, version),
271            time_to_string(self.end_time, version)
272        ))
273    }
274}
275
276#[derive(Clone, Debug, Hash, PartialEq, Eq)]
277pub struct ColourTransformation {
278    pub start_time: Integer,
279    pub red: u8,
280    pub green: u8,
281    pub blue: u8,
282}
283
284pub const COLOUR_TRANSFORMATION_HEADER: &str = "3";
285
286impl VersionedFromStr for ColourTransformation {
287    type Err = ParseColourTransformationError;
288
289    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
290        let (_, (start_time, red, green, blue)) = tuple((
291            preceded(
292                tuple((
293                    context(
294                        ParseColourTransformationError::WrongEventType.into(),
295                        tag(COLOUR_TRANSFORMATION_HEADER),
296                    ),
297                    context(
298                        ParseColourTransformationError::MissingStartTime.into(),
299                        comma(),
300                    ),
301                )),
302                start_time_offset(
303                    ParseColourTransformationError::InvalidStartTime.into(),
304                    version,
305                ),
306            ),
307            preceded(
308                context(ParseColourTransformationError::MissingRed.into(), comma()),
309                context(
310                    ParseColourTransformationError::InvalidRed.into(),
311                    comma_field_type(),
312                ),
313            ),
314            preceded(
315                context(ParseColourTransformationError::MissingGreen.into(), comma()),
316                context(
317                    ParseColourTransformationError::InvalidGreen.into(),
318                    comma_field_type(),
319                ),
320            ),
321            preceded(
322                context(ParseColourTransformationError::MissingBlue.into(), comma()),
323                context(
324                    ParseColourTransformationError::InvalidBlue.into(),
325                    consume_rest_type(),
326                ),
327            ),
328        ))(s)?;
329
330        Ok(Some(ColourTransformation {
331            start_time,
332            red,
333            green,
334            blue,
335        }))
336    }
337}
338
339impl VersionedToString for ColourTransformation {
340    fn to_string(&self, version: Version) -> Option<String> {
341        if version < 14 {
342            Some(format!(
343                "{COLOUR_TRANSFORMATION_HEADER},{},{},{},{}",
344                time_to_string(self.start_time, version),
345                self.red,
346                self.green,
347                self.blue
348            ))
349        } else {
350            None
351        }
352    }
353}
354
355#[derive(Clone, Debug, Hash, PartialEq, Eq)]
356/// Legacy version of Sprite event.
357pub struct SpriteLegacy {
358    pub layer: LayerLegacy,
359    pub origin: OriginTypeLegacy,
360    pub file_name: FilePath,
361    pub position: Option<Position>,
362    pub commands: Vec<Command>,
363}
364
365pub const SPRITE_LEGACY_HEADER: &str = "4";
366
367impl VersionedFromStr for SpriteLegacy {
368    type Err = ParseSpriteLegacyError;
369
370    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
371        let (_, (layer, origin, (file_name, position))) = preceded(
372            tuple((
373                context(
374                    ParseSpriteLegacyError::WrongEventType.into(),
375                    tag(SPRITE_LEGACY_HEADER),
376                ),
377                context(ParseSpriteLegacyError::MissingLayer.into(), comma()),
378            )),
379            tuple((
380                context(
381                    ParseSpriteLegacyError::InvalidLayer.into(),
382                    comma_field_versioned_type(version),
383                ),
384                preceded(
385                    context(ParseSpriteLegacyError::MissingOrigin.into(), comma()),
386                    context(
387                        ParseSpriteLegacyError::InvalidOrigin.into(),
388                        comma_field_versioned_type(version),
389                    ),
390                ),
391                preceded(
392                    context(ParseSpriteLegacyError::MissingFileName.into(), comma()),
393                    file_name_and_position(
394                        ParseSpriteLegacyError::MissingX.into(),
395                        ParseSpriteLegacyError::InvalidX.into(),
396                        ParseSpriteLegacyError::MissingY.into(),
397                        ParseSpriteLegacyError::InvalidY.into(),
398                    ),
399                ),
400            )),
401        )(s)?;
402
403        Ok(Some(SpriteLegacy {
404            layer,
405            origin,
406            file_name,
407            position,
408            commands: Vec::new(),
409        }))
410    }
411}
412
413impl VersionedToString for SpriteLegacy {
414    fn to_string(&self, version: Version) -> Option<String> {
415        Some(format!(
416            "{SPRITE_LEGACY_HEADER},{},{},{}{}",
417            self.layer.to_string(version).unwrap(),
418            self.origin.to_string(version).unwrap(),
419            self.file_name.to_string(version).unwrap(),
420            position_str(&self.position),
421        ))
422    }
423}
424
425impl EventWithCommands for SpriteLegacy {
426    fn commands(&self) -> &[Command] {
427        &self.commands
428    }
429
430    fn commands_mut(&mut self) -> &mut Vec<Command> {
431        &mut self.commands
432    }
433
434    fn to_string_cmd(&self, version: Version) -> Option<String> {
435        self.to_string(version)
436    }
437}
438
439#[derive(Clone, Debug, Hash, PartialEq, Eq)]
440pub struct AnimationLegacy {
441    pub layer: LayerLegacy,
442    pub origin: OriginTypeLegacy,
443    pub file_name: FilePath,
444    pub position: Option<Position>,
445    pub commands: Vec<Command>,
446}
447
448pub const ANIMATION_LEGACY_HEADER: &str = "5";
449
450impl VersionedFromStr for AnimationLegacy {
451    type Err = ParseAnimationLegacyError;
452    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
453        let (_, (layer, origin, (file_name, position))) = preceded(
454            tuple((
455                context(
456                    ParseAnimationLegacyError::WrongEventType.into(),
457                    tag(ANIMATION_LEGACY_HEADER),
458                ),
459                context(ParseAnimationLegacyError::MissingLayer.into(), comma()),
460            )),
461            tuple((
462                context(
463                    ParseAnimationLegacyError::InvalidLayer.into(),
464                    comma_field_versioned_type(version),
465                ),
466                preceded(
467                    context(ParseAnimationLegacyError::MissingOrigin.into(), comma()),
468                    context(
469                        ParseAnimationLegacyError::InvalidOrigin.into(),
470                        comma_field_versioned_type(version),
471                    ),
472                ),
473                preceded(
474                    context(ParseAnimationLegacyError::MissingFileName.into(), comma()),
475                    file_name_and_position(
476                        ParseAnimationLegacyError::MissingX.into(),
477                        ParseAnimationLegacyError::InvalidX.into(),
478                        ParseAnimationLegacyError::MissingY.into(),
479                        ParseAnimationLegacyError::InvalidY.into(),
480                    ),
481                ),
482            )),
483        )(s)?;
484
485        Ok(Some(AnimationLegacy {
486            layer,
487            origin,
488            file_name,
489            position,
490            commands: Vec::new(),
491        }))
492    }
493}
494
495impl VersionedToString for AnimationLegacy {
496    fn to_string(&self, version: Version) -> Option<String> {
497        Some(format!(
498            "{ANIMATION_LEGACY_HEADER},{},{},{}{}",
499            self.layer.to_string(version).unwrap(),
500            self.origin.to_string(version).unwrap(),
501            self.file_name.to_string(version).unwrap(),
502            position_str(&self.position),
503        ))
504    }
505}
506
507impl EventWithCommands for AnimationLegacy {
508    fn commands(&self) -> &[Command] {
509        &self.commands
510    }
511    fn commands_mut(&mut self) -> &mut Vec<Command> {
512        &mut self.commands
513    }
514    fn to_string_cmd(&self, version: Version) -> Option<String> {
515        self.to_string(version)
516    }
517}
518
519#[derive(Clone, Debug, Hash, PartialEq, Eq)]
520pub struct SampleLegacy {
521    pub time: Decimal,
522    pub layer: LayerLegacy,
523    pub file_name: FilePath,
524    pub volume: Option<Volume>,
525    pub commands: Vec<Command>,
526}
527
528pub const SAMPLE_LEGACY_HEADER: &str = "6";
529
530impl VersionedFromStr for SampleLegacy {
531    type Err = ParseSampleLegacyError;
532    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
533        let (_, (time, layer, file_name, volume)) = preceded(
534            tuple((
535                context(
536                    ParseSampleLegacyError::WrongEventType.into(),
537                    tag(SAMPLE_LEGACY_HEADER),
538                ),
539                context(ParseSampleLegacyError::MissingTime.into(), comma()),
540            )),
541            tuple((
542                context(
543                    ParseSampleLegacyError::InvalidTime.into(),
544                    comma_field_type(),
545                ),
546                preceded(
547                    context(ParseSampleLegacyError::MissingLayer.into(), comma()),
548                    context(
549                        ParseSampleLegacyError::InvalidLayer.into(),
550                        comma_field_versioned_type(version),
551                    ),
552                ),
553                preceded(
554                    context(ParseSampleLegacyError::MissingFileName.into(), comma()),
555                    comma_field().map(|f| f.into()),
556                ),
557                alt((
558                    eof.map(|_| None),
559                    preceded(
560                        context(ParseSampleLegacyError::MissingVolume.into(), comma()),
561                        context(
562                            ParseSampleLegacyError::InvalidVolume.into(),
563                            consume_rest_versioned_type(version),
564                        ),
565                    )
566                    .map(Some),
567                )),
568            )),
569        )(s)?;
570
571        Ok(Some(SampleLegacy {
572            time,
573            layer,
574            file_name,
575            volume,
576            commands: Vec::new(),
577        }))
578    }
579}
580
581impl VersionedToString for SampleLegacy {
582    fn to_string(&self, version: Version) -> Option<String> {
583        Some(format!(
584            "{SAMPLE_LEGACY_HEADER},{},{},{}{}",
585            self.time,
586            self.layer.to_string(version).unwrap(),
587            self.file_name.to_string(version).unwrap(),
588            self.volume
589                .map(|v| format!(",{}", v.to_string(version).unwrap()))
590                .unwrap_or_default(),
591        ))
592    }
593}
594
595impl EventWithCommands for SampleLegacy {
596    fn commands(&self) -> &[Command] {
597        &self.commands
598    }
599
600    fn commands_mut(&mut self) -> &mut Vec<Command> {
601        &mut self.commands
602    }
603
604    fn to_string_cmd(&self, version: Version) -> Option<String> {
605        self.to_string(version)
606    }
607}