osu_file_parser/osu_file/events/
mod.rs

1pub mod audio_sample;
2pub mod error;
3pub mod normal_event;
4pub mod storyboard;
5
6use nom::branch::alt;
7use nom::combinator::{cut, eof, peek, success};
8use nom::sequence::tuple;
9use nom::Parser;
10use nom::{bytes::complete::tag, combinator::rest, sequence::preceded};
11
12use crate::events::storyboard::cmds::CommandProperties;
13use crate::helper::trait_ext::MapOptStringNewLine;
14use crate::osb::Variable;
15use crate::parsers::comma;
16
17use self::storyboard::cmds::Command;
18use self::storyboard::error::CommandPushError;
19use self::storyboard::{error::ParseObjectError, sprites::Object};
20
21use super::Version;
22use super::{types::Error, Integer, VersionedDefault, VersionedFromStr, VersionedToString};
23
24pub use audio_sample::*;
25pub use error::*;
26pub use normal_event::*;
27
28#[derive(Default, Clone, Debug, Hash, PartialEq, Eq)]
29pub struct Events(pub Vec<Event>);
30
31const OLD_VERSION_TIME_OFFSET: Integer = 24;
32
33impl VersionedFromStr for Events {
34    type Err = Error<ParseError>;
35
36    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
37        Events::from_str_variables(s, version, &[])
38    }
39}
40
41impl Events {
42    pub fn from_str_variables(
43        s: &str,
44        version: Version,
45        variables: &[Variable],
46    ) -> std::result::Result<Option<Self>, Error<ParseError>> {
47        let mut events = Events(Vec::new());
48
49        #[derive(Clone)]
50        enum NormalEventType {
51            Background,
52            Video,
53            Break,
54            ColourTransformation,
55            SpriteLegacy,
56            AnimationLegacy,
57            SampleLegacy,
58            Other,
59        }
60
61        let mut comment = preceded::<_, _, _, nom::error::Error<_>, _, _>(tag("//"), rest);
62        let background = || {
63            peek(tuple((
64                tag::<_, _, nom::error::Error<_>>(normal_event::BACKGROUND_HEADER),
65                cut(alt((eof, comma()))),
66            )))
67            .map(|_| NormalEventType::Background)
68        };
69        let video = || {
70            peek(tuple((
71                alt((
72                    tag(normal_event::VIDEO_HEADER),
73                    tag(normal_event::VIDEO_HEADER_LONG),
74                )),
75                cut(alt((eof, comma()))),
76            )))
77            .map(|_| NormalEventType::Video)
78        };
79        let break_ = || {
80            peek(tuple((
81                alt((
82                    tag(normal_event::BREAK_HEADER),
83                    tag(normal_event::BREAK_HEADER_LONG),
84                )),
85                cut(alt((eof, comma()))),
86            )))
87            .map(|_| NormalEventType::Break)
88        };
89        let colour_transformation = || {
90            peek(tuple((
91                tag(normal_event::COLOUR_TRANSFORMATION_HEADER),
92                cut(alt((eof, comma()))),
93            )))
94            .map(|_| NormalEventType::ColourTransformation)
95        };
96        let sprite_legacy = || {
97            peek(tuple((
98                tag(normal_event::SPRITE_LEGACY_HEADER),
99                cut(alt((eof, comma()))),
100            )))
101            .map(|_| NormalEventType::SpriteLegacy)
102        };
103        let animation_legacy = || {
104            peek(tuple((
105                tag(normal_event::ANIMATION_LEGACY_HEADER),
106                cut(alt((eof, comma()))),
107            )))
108            .map(|_| NormalEventType::AnimationLegacy)
109        };
110        let sample_legacy = || {
111            peek(tuple((
112                tag(normal_event::SAMPLE_LEGACY_HEADER),
113                cut(alt((eof, comma()))),
114            )))
115            .map(|_| NormalEventType::SampleLegacy)
116        };
117
118        for (line_index, line) in s.lines().enumerate() {
119            if line.trim().is_empty() {
120                continue;
121            }
122
123            if let Ok((_, comment)) = comment(line) {
124                events.0.push(Event::Comment(comment.to_string()));
125                continue;
126            }
127
128            let indent = line.chars().take_while(|c| *c == ' ' || *c == '_').count();
129
130            // its a storyboard command
131            if indent > 0 {
132                let cmd_parse = || {
133                    let line_without_header = match line.chars().position(|c| c == ',') {
134                        Some(i) => &line[i + 1..],
135                        None => line,
136                    };
137
138                    let mut line_with_variable: Option<String> = None;
139                    for variable in variables {
140                        let variable_full = format!("${}", variable.name);
141
142                        if line_without_header.contains(&variable_full) {
143                            let new_line = match line_with_variable {
144                                Some(line_with_variable) => {
145                                    line_with_variable.replace(&variable_full, &variable.value)
146                                }
147                                None => line.replace(&variable_full, &variable.value),
148                            };
149
150                            line_with_variable = Some(new_line);
151                        }
152                    }
153
154                    match line_with_variable {
155                        Some(line_with_variable) => Error::new_from_result_into(
156                            Command::from_str(&line_with_variable, version),
157                            line_index,
158                        ),
159                        None => Error::new_from_result_into(
160                            Command::from_str(line, version),
161                            line_index,
162                        ),
163                    }
164                };
165
166                match events.0.last_mut() {
167                    Some(event) => match event {
168                        Event::Background(bg) => {
169                            if let Some(cmd) = cmd_parse()? {
170                                Error::new_from_result_into(
171                                    bg.try_push_cmd(cmd, indent),
172                                    line_index,
173                                )?
174                            }
175                        }
176                        Event::Video(video) => {
177                            if let Some(cmd) = cmd_parse()? {
178                                Error::new_from_result_into(
179                                    video.try_push_cmd(cmd, indent),
180                                    line_index,
181                                )?
182                            }
183                        }
184                        Event::SpriteLegacy(sprite) => {
185                            if let Some(cmd) = cmd_parse()? {
186                                Error::new_from_result_into(
187                                    sprite.try_push_cmd(cmd, indent),
188                                    line_index,
189                                )?
190                            }
191                        }
192                        Event::AnimationLegacy(animation) => {
193                            if let Some(cmd) = cmd_parse()? {
194                                Error::new_from_result_into(
195                                    animation.try_push_cmd(cmd, indent),
196                                    line_index,
197                                )?
198                            }
199                        }
200                        Event::SampleLegacy(sample) => {
201                            if let Some(cmd) = cmd_parse()? {
202                                Error::new_from_result_into(
203                                    sample.try_push_cmd(cmd, indent),
204                                    line_index,
205                                )?
206                            }
207                        }
208                        Event::StoryboardObject(obj) => {
209                            if let Some(cmd) = cmd_parse()? {
210                                Error::new_from_result_into(
211                                    obj.try_push_cmd(cmd, indent),
212                                    line_index,
213                                )?
214                            }
215                        }
216                        _ => {
217                            return Err(Error::new(
218                                ParseError::StoryboardCmdWithNoSprite,
219                                line_index,
220                            ))
221                        }
222                    },
223                    _ => {
224                        return Err(Error::new(
225                            ParseError::StoryboardCmdWithNoSprite,
226                            line_index,
227                        ))
228                    }
229                }
230                continue;
231            }
232
233            // normal event trying
234            let (_, type_) = alt((
235                background(),
236                video(),
237                break_(),
238                colour_transformation(),
239                sprite_legacy(),
240                animation_legacy(),
241                sample_legacy(),
242                success(NormalEventType::Other),
243            ))(line)
244            .unwrap();
245
246            let res = match type_ {
247                NormalEventType::Background => Background::from_str(line, version)
248                    .map(|e| e.map(Event::Background))
249                    .map_err(ParseError::ParseBackgroundError),
250                NormalEventType::Video => Video::from_str(line, version)
251                    .map(|e| e.map(Event::Video))
252                    .map_err(ParseError::ParseVideoError),
253                NormalEventType::Break => Break::from_str(line, version)
254                    .map(|e| e.map(Event::Break))
255                    .map_err(ParseError::ParseBreakError),
256                NormalEventType::ColourTransformation => {
257                    ColourTransformation::from_str(line, version)
258                        .map(|e| e.map(Event::ColourTransformation))
259                        .map_err(ParseError::ParseColourTransformationError)
260                }
261                NormalEventType::SpriteLegacy => SpriteLegacy::from_str(line, version)
262                    .map(|e| e.map(Event::SpriteLegacy))
263                    .map_err(ParseError::ParseSpriteLegacyError),
264                NormalEventType::AnimationLegacy => AnimationLegacy::from_str(line, version)
265                    .map(|e| e.map(Event::AnimationLegacy))
266                    .map_err(ParseError::ParseAnimationLegacyError),
267                NormalEventType::SampleLegacy => SampleLegacy::from_str(line, version)
268                    .map(|e| e.map(Event::SampleLegacy))
269                    .map_err(ParseError::ParseSampleLegacyError),
270                NormalEventType::Other => {
271                    // is it a storyboard object?
272                    match Object::from_str(line, version) {
273                        Ok(e) => Ok(e.map(Event::StoryboardObject)),
274                        Err(err) => {
275                            if let ParseObjectError::UnknownObjectType = err {
276                                // try AudioSample
277                                AudioSample::from_str(line, version)
278                                    .map(|e| e.map(Event::AudioSample))
279                                    .map_err(|e| {
280                                        if let ParseAudioSampleError::WrongEvent = e {
281                                            ParseError::UnknownEventType
282                                        } else {
283                                            ParseError::ParseAudioSampleError(e)
284                                        }
285                                    })
286                            } else {
287                                Err(ParseError::ParseStoryboardObjectError(err))
288                            }
289                        }
290                    }
291                }
292            };
293
294            match res {
295                Ok(event) => {
296                    if let Some(event) = event {
297                        events.0.push(event)
298                    }
299                }
300                Err(e) => return Err(Error::new(e, line_index)),
301            }
302        }
303
304        Ok(Some(events))
305    }
306
307    pub fn to_string_variables(&self, version: Version, variables: &[Variable]) -> Option<String> {
308        let mut s = self
309            .0
310            .iter()
311            .map(|event| event.to_string_variables(version, variables));
312
313        Some(s.map_string_new_line())
314    }
315}
316
317impl VersionedToString for Events {
318    fn to_string(&self, version: Version) -> Option<String> {
319        self.to_string_variables(version, &[])
320    }
321}
322
323impl VersionedDefault for Events {
324    fn default(_: Version) -> Option<Self> {
325        Some(Events(Vec::new()))
326    }
327}
328
329#[derive(Clone, Debug, Hash, PartialEq, Eq)]
330#[non_exhaustive]
331/// All possible events types.
332pub enum Event {
333    Comment(String),
334    Background(Background),
335    Video(Video),
336    Break(Break),
337    ColourTransformation(ColourTransformation),
338    SpriteLegacy(SpriteLegacy),
339    AnimationLegacy(AnimationLegacy),
340    SampleLegacy(SampleLegacy),
341    StoryboardObject(Object),
342    AudioSample(AudioSample),
343}
344
345impl VersionedToString for Event {
346    fn to_string(&self, version: Version) -> Option<String> {
347        self.to_string_variables(version, &[])
348    }
349}
350
351impl Event {
352    pub fn to_string_variables(&self, version: Version, variables: &[Variable]) -> Option<String> {
353        match self {
354            Event::Comment(comment) => Some(format!("//{comment}")),
355            Event::Background(background) => background.to_string(version),
356            Event::Video(video) => video.to_string(version),
357            Event::Break(break_) => break_.to_string(version),
358            Event::ColourTransformation(colour_trans) => colour_trans.to_string(version),
359            Event::SpriteLegacy(sprite) => sprite.to_string_variables(version, variables),
360            Event::AnimationLegacy(animation) => animation.to_string_variables(version, variables),
361            Event::SampleLegacy(sample) => sample.to_string_variables(version, variables),
362            Event::StoryboardObject(object) => object.to_string_variables(version, variables),
363            Event::AudioSample(audio_sample) => Some(audio_sample.to_string(version).unwrap()),
364        }
365    }
366}
367
368fn commands_to_string_variables(
369    cmds: &[Command],
370    version: Version,
371    variables: &[Variable],
372) -> Option<String> {
373    let mut builder = Vec::new();
374    let mut indentation = 1usize;
375
376    for cmd in cmds {
377        builder.push(format!(
378            "{}{}",
379            " ".repeat(indentation),
380            cmd.to_string_variables(version, variables).unwrap()
381        ));
382
383        if let CommandProperties::Loop { commands, .. }
384        | CommandProperties::Trigger { commands, .. } = &cmd.properties
385        {
386            if commands.is_empty() {
387                continue;
388            }
389
390            let starting_indentation = indentation;
391            indentation += 1;
392
393            let mut current_cmds = commands;
394            let mut current_index = 0;
395            // stack of commands, index, and indentation
396            let mut cmds_stack = Vec::new();
397
398            loop {
399                let cmd = &current_cmds[current_index];
400                current_index += 1;
401
402                builder.push(format!(
403                    "{}{}",
404                    " ".repeat(indentation),
405                    cmd.to_string_variables(version, variables).unwrap()
406                ));
407                match &cmd.properties {
408                    CommandProperties::Loop { commands, .. }
409                    | CommandProperties::Trigger { commands, .. }
410                        if !commands.is_empty() =>
411                    {
412                        // save the current cmds and index
413                        // ignore if index is already at the end of the current cmds
414                        if current_index < current_cmds.len() {
415                            cmds_stack.push((current_cmds, current_index, indentation));
416                        }
417
418                        current_cmds = commands;
419                        current_index = 0;
420                        indentation += 1;
421                    }
422                    _ => {
423                        if current_index >= current_cmds.len() {
424                            // check for end of commands
425                            match cmds_stack.pop() {
426                                Some((last_cmds, last_index, last_indentation)) => {
427                                    current_cmds = last_cmds;
428                                    current_index = last_index;
429                                    indentation = last_indentation;
430                                }
431                                None => break,
432                            }
433                        }
434                    }
435                }
436            }
437
438            indentation = starting_indentation;
439        }
440    }
441
442    Some(builder.join("\n"))
443}
444
445pub trait EventWithCommands {
446    fn try_push_cmd(&mut self, cmd: Command, indentation: usize) -> Result<(), CommandPushError> {
447        if indentation == 1 {
448            // first match no loop required
449            self.commands_mut().push(cmd);
450            Ok(())
451        } else {
452            let mut last_cmd = match self.commands_mut().last_mut() {
453                Some(last_cmd) => last_cmd,
454                None => return Err(CommandPushError::InvalidIndentation(1, indentation)),
455            };
456
457            for i in 1..indentation {
458                last_cmd = if let CommandProperties::Loop { commands, .. }
459                | CommandProperties::Trigger { commands, .. } =
460                    &mut last_cmd.properties
461                {
462                    if i + 1 == indentation {
463                        // last item
464                        commands.push(cmd);
465                        return Ok(());
466                    } else {
467                        match commands.last_mut() {
468                            Some(sub_cmd) => sub_cmd,
469                            None => {
470                                return Err(CommandPushError::InvalidIndentation(
471                                    i - 1,
472                                    indentation,
473                                ))
474                            }
475                        }
476                    }
477                } else {
478                    return Err(CommandPushError::InvalidIndentation(1, indentation));
479                };
480            }
481
482            unreachable!();
483        }
484    }
485
486    fn commands(&self) -> &[Command];
487
488    fn commands_mut(&mut self) -> &mut Vec<Command>;
489
490    /// Returns the command as a `String`.
491    /// - Instead of making the command into a string using `Display` or `VersionedToString`, use this to get the command as a string.
492    fn to_string_cmd(&self, version: Version) -> Option<String>;
493
494    /// Returns the command as a `String`.
495    /// - Contains the commands as a string as well.
496    /// - Use this in the `to_string` method with an empty `variables` array.
497    fn to_string_variables(&self, version: Version, variables: &[Variable]) -> Option<String> {
498        match self.to_string_cmd(version) {
499            Some(s) => {
500                let cmds = match commands_to_string_variables(self.commands(), version, variables) {
501                    Some(mut cmds) => {
502                        if !cmds.is_empty() {
503                            cmds = format!("\n{cmds}");
504                        }
505
506                        cmds
507                    }
508                    None => return None,
509                };
510
511                Some(format!("{s}{cmds}"))
512            }
513            None => None,
514        }
515    }
516}