osu_file_parser/osu_file/events/storyboard/cmds/
mod.rs

1pub mod error;
2pub mod types;
3
4use std::fmt::Display;
5
6use super::error::*;
7use super::types::*;
8use crate::osb::Variable;
9use crate::osu_file::types::Decimal;
10use crate::osu_file::{Integer, Version, VersionedFromStr, VersionedToString};
11use crate::parsers::*;
12use crate::VersionedFrom;
13use nom::branch::alt;
14use nom::bytes::complete::{tag, take_while};
15use nom::combinator::*;
16use nom::error::context;
17use nom::multi::many0;
18use nom::sequence::*;
19use nom::Parser;
20
21pub use error::*;
22pub use types::*;
23
24#[derive(Clone, Debug, Hash, PartialEq, Eq)]
25pub struct Command {
26    pub start_time: Option<Integer>,
27    pub properties: CommandProperties,
28}
29
30fn continuing_to_string<T>(continuing: &[T]) -> String
31where
32    T: Display,
33{
34    if continuing.is_empty() {
35        String::new()
36    } else {
37        format!(
38            ",{}",
39            continuing
40                .iter()
41                .map(|field| field.to_string())
42                .collect::<Vec<_>>()
43                .join(",")
44        )
45    }
46}
47
48fn continuing_versioned_to_string<T>(continuing: &[T], version: Version) -> String
49where
50    T: VersionedToString,
51{
52    if continuing.is_empty() {
53        String::new()
54    } else {
55        format!(
56            ",{}",
57            continuing
58                .iter()
59                .map(|field| field.to_string(version).unwrap())
60                .collect::<Vec<_>>()
61                .join(",")
62        )
63    }
64}
65
66impl VersionedToString for Command {
67    fn to_string(&self, version: Version) -> Option<String> {
68        self.to_string_variables(version, &[])
69    }
70}
71
72impl Command {
73    pub fn to_string_variables(&self, version: Version, variables: &[Variable]) -> Option<String> {
74        let end_time_to_string =
75            |end_time: &Option<i32>| end_time.map_or("".to_string(), |t| t.to_string());
76        let variable_replace = |header, cmd: String| {
77            let mut cmd = cmd;
78
79            for variable in variables {
80                if cmd.contains(&variable.value) {
81                    cmd = cmd.replace(&variable.value, &format!("${}", variable.name));
82                }
83            }
84
85            format!("{header},{cmd}")
86        };
87        let start_time = self.start_time.map_or(String::new(), |t| t.to_string());
88
89        let cmd_str = match &self.properties {
90            CommandProperties::Fade {
91                easing,
92                end_time,
93                start_opacity,
94                continuing_opacities,
95            } => {
96                let cmd = format!(
97                    "{},{start_time},{},{start_opacity}{}",
98                    easing.to_string(version).unwrap(),
99                    end_time_to_string(end_time),
100                    continuing_to_string(continuing_opacities),
101                );
102
103                variable_replace("F", cmd)
104            }
105            CommandProperties::Move {
106                easing,
107                end_time,
108                positions_xy,
109            } => {
110                let cmd = format!(
111                    "{},{start_time},{},{positions_xy}",
112                    easing.to_string(version).unwrap(),
113                    end_time_to_string(end_time),
114                );
115
116                variable_replace("M", cmd)
117            }
118            CommandProperties::MoveX {
119                easing,
120                end_time,
121                start_x,
122                continuing_x,
123            } => {
124                let cmd = format!(
125                    "{},{start_time},{},{start_x}{}",
126                    easing.to_string(version).unwrap(),
127                    end_time_to_string(end_time),
128                    continuing_to_string(continuing_x),
129                );
130
131                variable_replace("MX", cmd)
132            }
133            CommandProperties::MoveY {
134                easing,
135                end_time,
136                start_y,
137                continuing_y,
138            } => {
139                let cmd = format!(
140                    "{},{start_time},{},{start_y}{}",
141                    easing.to_string(version).unwrap(),
142                    end_time_to_string(end_time),
143                    continuing_to_string(continuing_y),
144                );
145
146                variable_replace("MY", cmd)
147            }
148            CommandProperties::Scale {
149                easing,
150                end_time,
151                start_scale,
152                continuing_scales,
153            } => {
154                let cmd = format!(
155                    "{},{start_time},{},{start_scale}{}",
156                    easing.to_string(version).unwrap(),
157                    end_time_to_string(end_time),
158                    continuing_to_string(continuing_scales),
159                );
160
161                variable_replace("S", cmd)
162            }
163            CommandProperties::VectorScale {
164                easing,
165                end_time,
166                scales_xy,
167            } => {
168                let cmd = format!(
169                    "{},{start_time},{},{}",
170                    easing.to_string(version).unwrap(),
171                    end_time_to_string(end_time),
172                    scales_xy,
173                );
174
175                variable_replace("V", cmd)
176            }
177            CommandProperties::Rotate {
178                easing,
179                end_time,
180                start_rotation,
181                continuing_rotations,
182            } => {
183                let cmd = format!(
184                    "{},{start_time},{},{start_rotation}{}",
185                    easing.to_string(version).unwrap(),
186                    end_time_to_string(end_time),
187                    continuing_to_string(continuing_rotations),
188                );
189
190                variable_replace("R", cmd)
191            }
192            CommandProperties::Colour {
193                easing,
194                end_time,
195                colours,
196            } => {
197                let cmd = format!(
198                    "{},{start_time},{},{}",
199                    easing.to_string(version).unwrap(),
200                    end_time_to_string(end_time),
201                    colours.to_string(version).unwrap(),
202                );
203
204                variable_replace("C", cmd)
205            }
206            CommandProperties::Parameter {
207                easing,
208                end_time,
209                parameter,
210                continuing_parameters,
211            } => {
212                let cmd = format!(
213                    "{},{start_time},{},{}{}",
214                    easing.to_string(version).unwrap(),
215                    end_time_to_string(end_time),
216                    parameter.to_string(version).unwrap(),
217                    continuing_versioned_to_string(continuing_parameters, version),
218                );
219
220                variable_replace("P", cmd)
221            }
222            CommandProperties::Loop {
223                loop_count,
224                // ignore commands since its handled separately
225                commands: _,
226            } => {
227                let cmd = format!("{start_time},{loop_count}");
228
229                variable_replace("L", cmd)
230            }
231            CommandProperties::Trigger {
232                trigger_type,
233                end_time,
234                group_number,
235                // ignore commands since its handled separately
236                commands: _,
237            } => {
238                let cmd = format!(
239                    "{}{}{}{}{}",
240                    trigger_type.to_string(version).unwrap(),
241                    if self.start_time.is_some() || end_time.is_some() || group_number.is_some() {
242                        ",".to_string()
243                    } else {
244                        String::new()
245                    },
246                    match self.start_time {
247                        Some(t) =>
248                            if end_time.is_some() || group_number.is_some() {
249                                format!("{t},")
250                            } else {
251                                t.to_string()
252                            },
253                        None =>
254                            if end_time.is_some() || group_number.is_some() {
255                                ",".to_string()
256                            } else {
257                                String::new()
258                            },
259                    },
260                    match end_time {
261                        Some(t) =>
262                            if group_number.is_some() {
263                                format!("{t},")
264                            } else {
265                                t.to_string()
266                            },
267                        None =>
268                            if group_number.is_some() {
269                                ",".to_string()
270                            } else {
271                                String::new()
272                            },
273                    },
274                    group_number.map_or(String::new(), |group_number| group_number.to_string()),
275                );
276
277                variable_replace("T", cmd)
278            }
279        };
280
281        Some(cmd_str)
282    }
283}
284
285#[derive(Clone, Debug, Hash, PartialEq, Eq)]
286#[non_exhaustive]
287pub enum CommandProperties {
288    Fade {
289        easing: Easing,
290        end_time: Option<Integer>,
291        start_opacity: Decimal,
292        continuing_opacities: Vec<Decimal>,
293    },
294    Move {
295        easing: Easing,
296        end_time: Option<Integer>,
297        positions_xy: ContinuingFields<Decimal>,
298    },
299    MoveX {
300        easing: Easing,
301        end_time: Option<Integer>,
302        start_x: Decimal,
303        continuing_x: Vec<Decimal>,
304    },
305    MoveY {
306        easing: Easing,
307        end_time: Option<Integer>,
308        start_y: Decimal,
309        continuing_y: Vec<Decimal>,
310    },
311    Scale {
312        easing: Easing,
313        end_time: Option<Integer>,
314        start_scale: Decimal,
315        continuing_scales: Vec<Decimal>,
316    },
317    VectorScale {
318        easing: Easing,
319        end_time: Option<Integer>,
320        scales_xy: ContinuingFields<Decimal>,
321    },
322    Rotate {
323        easing: Easing,
324        end_time: Option<Integer>,
325        start_rotation: Decimal,
326        continuing_rotations: Vec<Decimal>,
327    },
328    Colour {
329        easing: Easing,
330        end_time: Option<Integer>,
331        colours: Colours,
332    },
333    Parameter {
334        easing: Easing,
335        end_time: Option<Integer>,
336        parameter: Parameter,
337        continuing_parameters: Vec<Parameter>,
338    },
339    Loop {
340        loop_count: u32,
341        commands: Vec<Command>,
342    },
343    Trigger {
344        trigger_type: TriggerType,
345        end_time: Option<Integer>,
346        group_number: Option<Integer>,
347        commands: Vec<Command>,
348    },
349}
350
351impl VersionedFromStr for Command {
352    type Err = ParseCommandError;
353
354    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
355        let indentation = take_while(|c: char| c == ' ' || c == '_');
356        let start_time = || {
357            preceded(
358                context(ParseCommandError::MissingStartTime.into(), comma()),
359                context(
360                    ParseCommandError::InvalidStartTime.into(),
361                    comma_field_type().map(Some),
362                ),
363            )
364        };
365        let end_time = || {
366            preceded(
367                context(ParseCommandError::MissingEndTime.into(), cut(comma())),
368                alt((
369                    verify(comma_field(), |s: &str| s.trim().is_empty()).map(|_| None),
370                    cut(
371                        context(ParseCommandError::InvalidEndTime.into(), comma_field_type())
372                            .map(Some),
373                    ),
374                )),
375            )
376        };
377        let easing = || {
378            cut(preceded(
379                context(ParseCommandError::MissingEasing.into(), comma()),
380                context(
381                    ParseCommandError::InvalidEasing.into(),
382                    map_opt(comma_field_type(), |easing| {
383                        <Easing as VersionedFrom<Integer>>::from(easing, version)
384                    }),
385                ),
386            ))
387        };
388        // cases for start_time and end_time
389        // ...,easing,start_time,end_time,...
390        // ...,easing,start_time,,...
391        // ...,easing,,,...
392        let easing_start_end_time = || {
393            tuple((
394                easing(),
395                alt((
396                    tuple((tag(",,"), peek(comma()))).map(|_| (None, None)),
397                    tuple((start_time(), end_time())),
398                )),
399            ))
400        };
401        let continuing_decimal_two_fields =
402            |command_type: &'static str,
403             missing_starting_first,
404             invalid_start_first,
405             missing_starting_second,
406             invalid_starting_second,
407             invalid_continuing| {
408                let continuing = alt((
409                    eof.map(|_| None),
410                    cut(preceded(comma(), comma_field_type()).map(Some)),
411                ));
412                let continuing = many0(preceded(comma(), tuple((comma_field_type(), continuing))));
413
414                preceded(
415                    tag(command_type),
416                    cut(tuple((
417                        easing_start_end_time(),
418                        preceded(
419                            context(missing_starting_first, comma()),
420                            context(invalid_start_first, comma_field_type()),
421                        ),
422                        preceded(
423                            context(missing_starting_second, comma()),
424                            context(invalid_starting_second, comma_field_type()),
425                        ),
426                        terminated(continuing, context(invalid_continuing, eof)),
427                    ))),
428                )
429            };
430        let continuing_decimal_fields =
431            |command_type: &'static str, missing_start, invalid_start, invalid_continuing| {
432                let continuing = many0(preceded(comma(), comma_field_type()));
433
434                preceded(
435                    tag(command_type),
436                    cut(tuple((
437                        easing_start_end_time(),
438                        preceded(
439                            context(missing_start, comma()),
440                            context(invalid_start, comma_field_type()),
441                        ),
442                        terminated(continuing, context(invalid_continuing, eof)),
443                    ))),
444                )
445            };
446
447        let loop_ = preceded(
448            tag("L"),
449            cut(tuple((
450                alt((tuple((comma(), peek(comma()))).map(|_| None), start_time())),
451                preceded(
452                    context(ParseCommandError::MissingLoopCount.into(), comma()),
453                    context(
454                        ParseCommandError::InvalidLoopCount.into(),
455                        map_res(rest, |s: &str| s.parse()),
456                    ),
457                ),
458            ))),
459        )
460        .map(|(start_time, loop_count)| Command {
461            start_time,
462            properties: CommandProperties::Loop {
463                loop_count,
464                commands: Vec::new(),
465            },
466        });
467        let trigger = {
468            let trigger_nothing = alt((
469                verify(rest, |s: &str| s.trim().is_empty()).map(|_| (None, None)),
470                verify(rest, |s: &str| s == ",").map(|_| (None, None)),
471            ));
472            let trigger_group_number = preceded(
473                tuple((comma(), comma())),
474                context(
475                    ParseCommandError::InvalidGroupNumber.into(),
476                    cut(consume_rest_type()),
477                ),
478            )
479            .map(|group_number| (None, Some(group_number)));
480            let trigger_end_time = preceded(
481                comma(),
482                context(
483                    ParseCommandError::InvalidEndTime.into(),
484                    cut(consume_rest_type()),
485                ),
486            )
487            .map(|end_time| (Some(end_time), None));
488            let trigger_everything = tuple((
489                preceded(comma(), comma_field_type()),
490                preceded(
491                    comma(),
492                    context(
493                        ParseCommandError::InvalidGroupNumber.into(),
494                        cut(consume_rest_type()),
495                    ),
496                ),
497            ))
498            .map(|(end_time, group_number)| (Some(end_time), Some(group_number)));
499
500            preceded(
501                tuple((
502                    tag("T"),
503                    context(ParseCommandError::MissingTriggerType.into(), cut(comma())),
504                )),
505                cut(tuple((
506                    context(
507                        ParseCommandError::InvalidTriggerType.into(),
508                        map_res(comma_field(), |s| {
509                            TriggerType::from_str(s, version).map(|t| t.unwrap())
510                        }),
511                    ),
512                    alt((
513                        tuple((comma(), peek(comma()))).map(|_| None),
514                        verify(rest, |s: &str| s.trim().is_empty()).map(|_| None),
515                        start_time(),
516                    )),
517                    // there are 4 possibilities:
518                    alt((
519                        // has everything
520                        trigger_everything,
521                        // has group number
522                        trigger_group_number,
523                        // nothing
524                        trigger_nothing,
525                        // has end time
526                        trigger_end_time,
527                    )),
528                ))),
529            )
530            .map(
531                |(trigger_type, start_time, (end_time, group_number))| Command {
532                    start_time,
533                    properties: CommandProperties::Trigger {
534                        trigger_type,
535                        end_time,
536                        group_number,
537                        commands: Vec::new(),
538                    },
539                },
540            )
541        };
542        let colour = {
543            let continuing_colour = || {
544                alt((
545                    eof.map(|_| None),
546                    preceded(comma(), comma_field_type()).map(Some),
547                ))
548            };
549            let continuing_colours = many0(preceded(
550                comma(),
551                tuple((comma_field_type(), continuing_colour(), continuing_colour())),
552            ));
553
554            preceded(
555                tag("C"),
556                cut(tuple((
557                    easing_start_end_time(),
558                    preceded(
559                        context(ParseCommandError::MissingRed.into(), comma()),
560                        context(ParseCommandError::InvalidRed.into(), comma_field_type()),
561                    ),
562                    preceded(
563                        context(ParseCommandError::MissingGreen.into(), comma()),
564                        context(ParseCommandError::InvalidGreen.into(), comma_field_type()),
565                    ),
566                    preceded(
567                        context(ParseCommandError::MissingBlue.into(), comma()),
568                        context(ParseCommandError::InvalidBlue.into(), comma_field_type()),
569                    ),
570                    terminated(
571                        continuing_colours,
572                        context(ParseCommandError::InvalidContinuingColours.into(), eof),
573                    ),
574                ))),
575            )
576            .map(
577                |((easing, (start_time, end_time)), start_r, start_g, start_b, continuing)| {
578                    Command {
579                        start_time,
580                        properties: CommandProperties::Colour {
581                            easing,
582                            end_time,
583                            colours: Colours {
584                                start: (start_r, start_g, start_b),
585                                continuing,
586                            },
587                        },
588                    }
589                },
590            )
591        };
592        let parameter = {
593            let continuing_parameters =
594                many0(preceded(comma(), comma_field_versioned_type(version)));
595
596            preceded(
597                tag("P"),
598                cut(tuple((
599                    easing_start_end_time(),
600                    preceded(
601                        context(ParseCommandError::MissingParameterType.into(), comma()),
602                        context(
603                            ParseCommandError::InvalidParameterType.into(),
604                            comma_field_versioned_type(version),
605                        ),
606                    ),
607                    terminated(
608                        continuing_parameters,
609                        context(ParseCommandError::InvalidContinuingParameters.into(), eof),
610                    ),
611                ))),
612            )
613            .map(
614                |((easing, (start_time, end_time)), parameter, continuing_parameters)| Command {
615                    start_time,
616                    properties: CommandProperties::Parameter {
617                        easing,
618                        end_time,
619                        parameter,
620                        continuing_parameters,
621                    },
622                },
623            )
624        };
625        let move_ = {
626            {
627                let continuing = alt((
628                    eof.map(|_| None),
629                    cut(preceded(comma(), comma_field_type()).map(Some)),
630                ));
631                let continuing = many0(preceded(comma(), tuple((comma_field_type(), continuing))));
632
633                preceded(
634                    tuple(((tag("M")), peek(comma()))),
635                    cut(tuple((
636                        easing_start_end_time(),
637                        preceded(
638                            context(ParseCommandError::MissingMoveX.into(), comma()),
639                            context(ParseCommandError::InvalidMoveX.into(), comma_field_type()),
640                        ),
641                        preceded(
642                            context(ParseCommandError::MissingMoveY.into(), comma()),
643                            context(ParseCommandError::InvalidMoveY.into(), comma_field_type()),
644                        ),
645                        terminated(
646                            continuing,
647                            context(ParseCommandError::InvalidContinuingMove.into(), eof),
648                        ),
649                    ))),
650                )
651            }
652        }
653        .map(
654            |((easing, (start_time, end_time)), start_x, start_y, continuing)| Command {
655                start_time,
656                properties: CommandProperties::Move {
657                    easing,
658                    end_time,
659                    positions_xy: ContinuingFields {
660                        start: (start_x, start_y),
661                        continuing,
662                    },
663                },
664            },
665        );
666        let vector_scale = continuing_decimal_two_fields(
667            "V",
668            ParseCommandError::MissingScaleX.into(),
669            ParseCommandError::InvalidScaleX.into(),
670            ParseCommandError::MissingScaleY.into(),
671            ParseCommandError::InvalidScaleY.into(),
672            ParseCommandError::InvalidContinuingScales.into(),
673        )
674        .map(
675            |((easing, (start_time, end_time)), start_x, start_y, continuing)| Command {
676                start_time,
677                properties: CommandProperties::VectorScale {
678                    easing,
679                    end_time,
680                    scales_xy: ContinuingFields {
681                        start: (start_x, start_y),
682                        continuing,
683                    },
684                },
685            },
686        );
687        let fade = continuing_decimal_fields(
688            "F",
689            ParseCommandError::MissingStartOpacity.into(),
690            ParseCommandError::InvalidStartOpacity.into(),
691            ParseCommandError::InvalidContinuingOpacities.into(),
692        )
693        .map(
694            |((easing, (start_time, end_time)), start_opacity, continuing_opacities)| Command {
695                start_time,
696                properties: CommandProperties::Fade {
697                    easing,
698                    end_time,
699                    start_opacity,
700                    continuing_opacities,
701                },
702            },
703        );
704        let move_x = continuing_decimal_fields(
705            "MX",
706            ParseCommandError::MissingMoveX.into(),
707            ParseCommandError::InvalidMoveX.into(),
708            ParseCommandError::InvalidContinuingMove.into(),
709        )
710        .map(
711            |((easing, (start_time, end_time)), start_x, continuing_x)| Command {
712                start_time,
713                properties: CommandProperties::MoveX {
714                    easing,
715                    end_time,
716                    start_x,
717                    continuing_x,
718                },
719            },
720        );
721        let move_y = continuing_decimal_fields(
722            "MY",
723            ParseCommandError::MissingMoveY.into(),
724            ParseCommandError::InvalidMoveY.into(),
725            ParseCommandError::InvalidContinuingMove.into(),
726        )
727        .map(
728            |((easing, (start_time, end_time)), start_y, continuing_y)| Command {
729                start_time,
730                properties: CommandProperties::MoveY {
731                    easing,
732                    end_time,
733                    start_y,
734                    continuing_y,
735                },
736            },
737        );
738        let scale = continuing_decimal_fields(
739            "S",
740            ParseCommandError::MissingStartScale.into(),
741            ParseCommandError::InvalidStartScale.into(),
742            ParseCommandError::InvalidContinuingScales.into(),
743        )
744        .map(
745            |((easing, (start_time, end_time)), start_scale, continuing_scales)| Command {
746                start_time,
747                properties: CommandProperties::Scale {
748                    easing,
749                    end_time,
750                    start_scale,
751                    continuing_scales,
752                },
753            },
754        );
755        let rotate = continuing_decimal_fields(
756            "R",
757            ParseCommandError::MissingStartRotation.into(),
758            ParseCommandError::InvalidStartRotation.into(),
759            ParseCommandError::InvalidContinuingRotation.into(),
760        )
761        .map(
762            |((easing, (start_time, end_time)), start_rotation, continuing_rotations)| Command {
763                start_time,
764                properties: CommandProperties::Rotate {
765                    easing,
766                    end_time,
767                    start_rotation,
768                    continuing_rotations,
769                },
770            },
771        );
772
773        // we order by the most common to the least common
774        let parse = preceded(
775            indentation,
776            // note: if adding new command, make sure to check if the char is conflicting
777            // if it is, make sure we peek for a comma after the tag check
778            alt((
779                move_,
780                rotate,
781                scale,
782                vector_scale,
783                fade,
784                parameter,
785                move_y,
786                colour,
787                move_x,
788                trigger,
789                loop_,
790                context(ParseCommandError::UnknownCommandType.into(), fail),
791            )),
792        )(s)?;
793
794        Ok(Some(parse.1))
795    }
796}