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 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 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 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 alt((
519 trigger_everything,
521 trigger_group_number,
523 trigger_nothing,
525 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 let parse = preceded(
775 indentation,
776 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}