mugen_air/
lib.rs

1use std::{collections::VecDeque, fmt::Display, str::FromStr};
2
3use nom::{
4    branch::alt,
5    bytes::complete::{is_not, tag, tag_no_case},
6    character::complete::{
7        char, digit1, i32, line_ending, multispace0, one_of, space0, space1, u32,
8    },
9    combinator::{eof, map, map_res, opt, success, value},
10    multi::{many0, many1, separated_list1},
11    sequence::{delimited, preceded, terminated, tuple},
12    Finish, IResult,
13};
14use nom_locate::LocatedSpan;
15
16type Span<'a> = LocatedSpan<&'a str>;
17type ParseResult<'a, T> = IResult<Span<'a>, T>;
18
19#[derive(Debug)]
20pub struct Action {
21    pub name: String,
22    pub elements: Vec<Frame>,
23    pub loop_start: usize,
24}
25
26#[derive(Debug)]
27enum Element {
28    Frame(Frame),
29    LoopStart,
30    HurtBoxes {
31        default: bool,
32        boxes: Vec<CollisionBox>,
33    },
34    HitBoxes {
35        default: bool,
36        boxes: Vec<CollisionBox>,
37    },
38}
39
40#[derive(Debug)]
41pub struct Frame {
42    pub group: u32,
43    pub image: i32,
44    pub x: i32,
45    pub y: i32,
46    pub ticks: i32,
47    pub flip: Flip,
48    pub blend: Option<Blend>,
49    pub hit_boxes: Vec<CollisionBox>,
50    pub hurt_boxes: Vec<CollisionBox>,
51}
52
53#[derive(Debug, PartialEq, Eq, Clone, Copy)]
54pub enum Flip {
55    NoFlip,
56    Horizontal,
57    Vertical,
58    Both,
59}
60
61#[derive(Debug, PartialEq, Eq, Clone, Copy)]
62pub enum Blend {
63    Add { src: u32, dst: u32 },
64    Sub,
65}
66
67#[derive(Debug, PartialEq, Eq, Clone, Copy)]
68pub struct CollisionBox(i32, i32, i32, i32);
69
70impl CollisionBox {
71    pub fn x(&self) -> i32 {
72        self.0
73    }
74
75    pub fn y(&self) -> i32 {
76        self.1
77    }
78
79    pub fn width(&self) -> i32 {
80        self.2 - self.0
81    }
82
83    pub fn height(&self) -> i32 {
84        self.3 - self.1
85    }
86}
87
88#[derive(Debug)]
89pub struct ParseError {
90    pub line: u32,
91}
92
93impl FromStr for Action {
94    type Err = ParseError;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        match single_action(s.into()).finish() {
98            Ok((_, action)) => Ok(action),
99            Err(err) => Err(ParseError {
100                line: err.input.location_line(),
101            }),
102        }
103    }
104}
105
106pub fn parse_air(air: &str) -> Result<Vec<Action>, ParseError> {
107    match actions(air.into()).finish() {
108        Ok((_, actions)) => Ok(actions),
109        Err(err) => Err(ParseError {
110            line: err.input.location_line(),
111        }),
112    }
113}
114
115fn actions(i: Span) -> ParseResult<Vec<Action>> {
116    terminated(many0(action), ending)(i)
117}
118
119fn single_action(i: Span) -> ParseResult<Action> {
120    terminated(action, ending)(i)
121}
122
123fn ending(i: Span) -> ParseResult<Span> {
124    alt((
125        eof,
126        value("".into(), tuple((multispace0, eof))),
127        value(
128            "".into(),
129            tuple((multispace0, many0(comment_or_eol), multispace0, eof)),
130        ),
131    ))(i)
132}
133
134fn action(i: Span) -> ParseResult<Action> {
135    let (i, name) = delimited(
136        tuple((multispace0, many0(comment_or_eol), multispace0)),
137        begin_action,
138        tuple((space0, comment_or_eol)),
139    )(i)?;
140
141    let (i, elements) = many1(preceded(
142        tuple((multispace0, many0(comment_or_eol), multispace0)),
143        animation_element,
144    ))(i)?;
145
146    let mut elements: VecDeque<_> = elements.into();
147    let mut frames = Vec::with_capacity(elements.len());
148    let mut loop_start = 0;
149    let mut time = 0;
150    let mut hit_boxes: (bool, Option<Vec<CollisionBox>>) = Default::default();
151    let mut hurt_boxes: (bool, Option<Vec<CollisionBox>>) = Default::default();
152
153    while let Some(elem) = elements.pop_front() {
154        match elem {
155            Element::Frame(frame) => {
156                let frame = Frame {
157                    hit_boxes: match hit_boxes {
158                        (true, ref boxes) => boxes.clone().unwrap_or_default(),
159                        (false, ref mut boxes) => boxes.take().unwrap_or_default(),
160                    },
161                    hurt_boxes: match hurt_boxes {
162                        (true, ref boxes) => boxes.clone().unwrap_or_default(),
163                        (false, ref mut boxes) => boxes.take().unwrap_or_default(),
164                    },
165                    ..frame
166                };
167                if frame.ticks >= 0 {
168                    time += frame.ticks;
169                    frames.push(frame);
170                } else {
171                    frames.push(frame);
172                    break;
173                }
174            }
175            Element::LoopStart => loop_start = time,
176            Element::HurtBoxes { default, boxes } => {
177                hurt_boxes = (default, Some(boxes));
178            }
179            Element::HitBoxes { default, boxes } => {
180                hit_boxes = (default, Some(boxes));
181            }
182        }
183    }
184
185    Ok((
186        i,
187        Action {
188            name: name.to_string(),
189            elements: frames,
190            loop_start: loop_start as usize,
191        },
192    ))
193}
194
195fn comment(i: Span) -> ParseResult<LocatedSpan<&str>> {
196    preceded(tag(";"), is_not("\n"))(i)
197}
198
199fn comment_or_eol(i: Span) -> ParseResult<LocatedSpan<&str>> {
200    alt((comment, line_ending, eof))(i)
201}
202
203fn begin_action(i: Span) -> ParseResult<Span> {
204    delimited(char('['), delimited(space0, action_name, space0), char(']'))(i)
205}
206
207fn action_name(i: Span) -> ParseResult<Span> {
208    let (i, _) = tag_no_case("begin")(i)?;
209    let (i, _) = space1(i)?;
210    let (i, _) = tag_no_case("action")(i)?;
211    let (i, _) = space1(i)?;
212    digit1(i)
213}
214
215fn animation_element(i: Span) -> ParseResult<Element> {
216    let frame = map(animation_frame, Element::Frame);
217    let loop_start = map(tag_no_case("loopstart"), |_| Element::LoopStart);
218    alt((
219        terminated(frame, tuple((space0, comment_or_eol))),
220        terminated(loop_start, tuple((space0, comment_or_eol))),
221        clsn_boxes,
222    ))(i)
223}
224
225fn animation_frame(i: Span) -> ParseResult<Frame> {
226    let (i, (group, image, x, y, ticks, flip, blend)) = tuple((
227        terminated(delimited(space0, u32, space0), char(',')),
228        terminated(delimited(space0, i32, space0), char(',')),
229        terminated(delimited(space0, i32, space0), char(',')),
230        terminated(delimited(space0, i32, space0), char(',')),
231        delimited(space0, i32, space0),
232        alt((
233            delimited(
234                delimited(space0, char(','), space0),
235                opt(element_flip),
236                space0,
237            ),
238            success(None),
239        )),
240        alt((
241            delimited(
242                delimited(space0, char(','), space0),
243                opt(element_blend),
244                space0,
245            ),
246            success(None),
247        )),
248    ))(i)?;
249
250    let elem = Frame {
251        group,
252        image,
253        x,
254        y,
255        ticks,
256        flip: flip.unwrap_or(Flip::NoFlip),
257        blend,
258        hit_boxes: Vec::new(),
259        hurt_boxes: Vec::new(),
260    };
261
262    Ok((i, elem))
263}
264
265fn element_flip(i: Span) -> ParseResult<Flip> {
266    let flip = alt((
267        tag_no_case("VH"),
268        tag_no_case("HV"),
269        tag_no_case("V"),
270        tag_no_case("H"),
271    ));
272
273    map_res(flip, |flip: Span| {
274        match flip.to_ascii_uppercase().as_str() {
275            "VH" | "HV" => Ok(Flip::Both),
276            "H" => Ok(Flip::Horizontal),
277            "V" => Ok(Flip::Vertical),
278            _ => Err("Invalid flip"),
279        }
280    })(i)
281}
282
283fn element_blend(i: Span) -> ParseResult<Blend> {
284    fn add_blend(i: Span) -> ParseResult<Blend> {
285        alt((full_add_blend, short_add_blend))(i)
286    }
287
288    fn short_add_blend(i: Span) -> ParseResult<Blend> {
289        let (i, _) = tag_no_case("A")(i)?;
290        let (i, dst) = opt(u32)(i)?;
291        let dst = match dst {
292            Some(n) => 256 / 2_u32.pow(n),
293            _ => 256,
294        };
295
296        Ok((i, Blend::Add { src: 256, dst }))
297    }
298
299    fn full_add_blend(i: Span) -> ParseResult<Blend> {
300        let (i, _) = tag_no_case("AS")(i)?;
301        let (i, src) = u32(i)?;
302        let (i, _) = tag_no_case("D")(i)?;
303        let (i, dst) = u32(i)?;
304
305        Ok((i, Blend::Add { src, dst }))
306    }
307
308    fn sub_blend(i: Span) -> ParseResult<Blend> {
309        let (i, _) = tag_no_case("S")(i)?;
310        Ok((i, Blend::Sub))
311    }
312
313    alt((add_blend, sub_blend))(i)
314}
315
316fn clsn_boxes(i: Span) -> ParseResult<Element> {
317    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
318    enum ClsnBoxKind {
319        Hit,
320        Hurt,
321    }
322
323    impl Display for ClsnBoxKind {
324        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325            match self {
326                ClsnBoxKind::Hit => write!(f, "1"),
327                ClsnBoxKind::Hurt => write!(f, "2"),
328            }
329        }
330    }
331
332    fn clsn_box_kind(i: Span) -> ParseResult<ClsnBoxKind> {
333        map_res(one_of("12"), |k| match k {
334            '1' => Ok(ClsnBoxKind::Hit),
335            '2' => Ok(ClsnBoxKind::Hurt),
336            k => Err(format!("Invalid collision box kind: {k}")),
337        })(i)
338    }
339
340    fn clsn_box(expected_kind: ClsnBoxKind) -> impl Fn(Span) -> ParseResult<CollisionBox> {
341        move |i| {
342            let (i, _) = tag_no_case("Clsn")(i)?;
343            let (i, _) = map_res(clsn_box_kind, |found_kind| {
344                if expected_kind == found_kind {
345                    Ok(found_kind)
346                } else {
347                    Err(format!(
348                        "Expecting collision box kind {expected_kind} found: {found_kind}"
349                    ))
350                }
351            })(i)?;
352            let (i, _n) = delimited(tag("["), delimited(space0, u32, space0), tag("]"))(i)?;
353            let (i, _) = preceded(space0, tag("="))(i)?;
354            let (i, coords) = map_res(
355                separated_list1(tag(","), delimited(space0, i32, space0)),
356                |coords| {
357                    if coords.len() == 4 {
358                        Ok(coords)
359                    } else {
360                        Err("Invalid collision box")
361                    }
362                },
363            )(i)?;
364
365            Ok((i, CollisionBox(coords[0], coords[1], coords[2], coords[3])))
366        }
367    }
368
369    let (i, _) = tag_no_case("Clsn")(i)?;
370    let (i, kind) = clsn_box_kind(i)?;
371    let (i, def) = opt(tag_no_case("default"))(i)?;
372    let (i, _) = preceded(space0, tag(":"))(i)?;
373
374    let (i, count) = preceded(space0, u32)(i)?;
375
376    let (i, boxes) = map_res(
377        many1(preceded(
378            tuple((multispace0, many0(comment_or_eol), multispace0)),
379            clsn_box(kind),
380        )),
381        |boxes| {
382            if count as usize == boxes.len() {
383                Ok(boxes)
384            } else {
385                Err(format!(
386                    "Expecting {count} collision boxes, found: {}",
387                    boxes.len()
388                ))
389            }
390        },
391    )(i)?;
392
393    let element = match kind {
394        ClsnBoxKind::Hit => Element::HitBoxes {
395            default: def.is_some(),
396            boxes,
397        },
398        ClsnBoxKind::Hurt => Element::HurtBoxes {
399            default: def.is_some(),
400            boxes,
401        },
402    };
403    Ok((i, element))
404}
405
406#[cfg(test)]
407mod tests {
408    use indoc::indoc;
409
410    use super::*;
411
412    #[test]
413    fn it_shows_location_of_the_error() {
414        let text = "[begin action invalid]";
415        let error = text.parse::<Action>().err().unwrap();
416        assert_eq!(error.line, 1);
417
418        let text = indoc! {"
419            [begin action 001]
420            200, 20, 30, 40, 1
421            200, 20, 30, 40, invalid
422        "};
423        let error = text.parse::<Action>().err().unwrap();
424        assert_eq!(error.line, 3);
425
426        let text = indoc! {"
427            [begin action 001]
428            200, 20, 30, 40, 1
429            200, 20, 30, 40, 2
430
431            [begin action 002]
432            200, 20, 30, 40, 1
433            200, 20, 30, 40, 2
434
435            [begin action 003]
436            200, 20, 30, 40, 1
437            200, 20, 30, 40, 2
438        "};
439        let error = text.parse::<Action>().err().unwrap();
440        assert_eq!(error.line, 5);
441
442        let text = indoc! {"
443            [begin action 001]
444            200, 20, 30, 40, 1
445            200, 20, 30, 40, 2
446
447            [begin action 002]
448            200, 20, 30, 40, 1
449            200, 20, 30, 40, 2
450
451            [begin action 003]
452            200, 20, 30, 40, 1
453            200, 20, 30, 40, invalid
454
455            [begin action 004]
456            200, 20, 30, 40, 1
457        "};
458        let error = parse_air(text).err().unwrap();
459        assert_eq!(error.line, 11);
460    }
461
462    #[test]
463    fn it_parses_air_file() {
464        let text = indoc! {"
465            [begin action 001]
466            200, 20, 30, 40, 50
467
468            [begin action 002]
469            200, 20, 30, 40, 50
470
471            ; Action 3
472            [begin action 003]
473            200, 20, 30, 40, 50
474
475        "};
476        let actions = parse_air(text).unwrap();
477        assert_eq!(actions.len(), 3);
478        assert_eq!(actions[0].name, "001");
479        assert_eq!(actions[1].name, "002");
480        assert_eq!(actions[2].name, "003");
481    }
482
483    #[test]
484    fn it_parses_action_name() {
485        let text = indoc! {"
486            [begin action 001]
487            200, 20, 30, 40, 50
488        "};
489        let action = text.parse::<Action>().unwrap();
490        assert_eq!(action.name, "001");
491
492        let text = indoc! {"
493            [  begin   action  001  ]
494            200, 20, 30, 40, 50
495        "};
496        let action = text.parse::<Action>().unwrap();
497        assert_eq!(action.name, "001");
498
499        let text = indoc! {"
500               [begin action 001]
501            200, 20, 30, 40, 50
502        "};
503        let action = text.parse::<Action>().unwrap();
504        assert_eq!(action.name, "001");
505
506        let text = indoc! {"
507
508
509            ; Comment
510
511               [begin action 001]
512            200, 20, 30, 40, 50
513        "};
514        let action = text.parse::<Action>().unwrap();
515        assert_eq!(action.name, "001");
516    }
517
518    #[test]
519    fn it_parses_animation_elements() {
520        let text = indoc! {"
521            [begin action 001]
522            200, 20, 30, 40, 50
523        "};
524        let action = text.parse::<Action>().unwrap();
525        assert_eq!(action.elements.len(), 1);
526        let element = &action.elements[0];
527        assert_eq!(element.group, 200);
528        assert_eq!(element.image, 20);
529        assert_eq!(element.x, 30);
530        assert_eq!(element.y, 40);
531        assert_eq!(element.ticks, 50);
532    }
533
534    #[test]
535    fn it_parses_animation_element_flip() {
536        let text = indoc! {"
537            [begin action 001]
538            200, 20, 30, 40, 50
539            200, 20, 30, 40, 50,
540            200, 20, 30, 40, 50, V
541            200, 20, 30, 40, 50, H
542            200, 20, 30, 40, 50, VH
543            200, 20, 30, 40, 50, HV
544        "};
545        let action = text.parse::<Action>().unwrap();
546        assert_eq!(action.elements[0].flip, Flip::NoFlip);
547        assert_eq!(action.elements[1].flip, Flip::NoFlip);
548        assert_eq!(action.elements[2].flip, Flip::Vertical);
549        assert_eq!(action.elements[3].flip, Flip::Horizontal);
550        assert_eq!(action.elements[4].flip, Flip::Both);
551        assert_eq!(action.elements[5].flip, Flip::Both);
552
553        let text = indoc! {"
554            [begin action 001]
555            200, 20, 30, 40, 50, O
556        "};
557        assert!(text.parse::<Action>().is_err());
558    }
559
560    #[test]
561    fn it_parses_animation_element_blend() {
562        let text = indoc! {"
563            [begin action 001]
564            200, 20, 30, 40, 50
565            200, 20, 30, 40, 50, , A
566            200, 20, 30, 40, 50, , S
567            200, 20, 30, 40, 50, , A1
568            200, 20, 30, 40, 50, , A2
569            200, 20, 30, 40, 50, , A3
570            200, 20, 30, 40, 50, , AS33D44
571        "};
572        let action = text.parse::<Action>().unwrap();
573        let elems = &action.elements;
574        assert_eq!(elems[0].blend, None);
575        assert_eq!(elems[1].blend, Some(Blend::Add { src: 256, dst: 256 }));
576        assert_eq!(elems[2].blend, Some(Blend::Sub));
577        assert_eq!(elems[3].blend, Some(Blend::Add { src: 256, dst: 128 }));
578        assert_eq!(elems[4].blend, Some(Blend::Add { src: 256, dst: 64 }));
579        assert_eq!(elems[5].blend, Some(Blend::Add { src: 256, dst: 32 }));
580        assert_eq!(elems[6].blend, Some(Blend::Add { src: 33, dst: 44 }));
581
582        let text = indoc! {"
583            [begin action 001]
584            200, 20, 30, 40, 50, , invalid
585        "};
586        assert!(text.parse::<Action>().is_err());
587    }
588
589    #[test]
590    fn it_parses_loop_start() {
591        let text = indoc! {"
592            [begin action 001]
593            200, 10, 30, 40, 50
594            200, 20, 30, 40, 50
595             loopstart
596            200, 30, 30, 40, 50
597            200, 40, 30, 40, 50
598        "};
599        let action = text.parse::<Action>().unwrap();
600        assert_eq!(action.loop_start, 100);
601    }
602
603    #[test]
604    fn it_parses_collision_boxes() {
605        let text = indoc! {"
606            [begin action 001]
607            Clsn2: 2
608             Clsn2[0] =  15, -1,-24,-72
609             Clsn2[1] = -12,-84, 11,-61
610            200, 20, 30, 40, 50
611            200, 30, 30, 40, 50
612        "};
613        let action = text.parse::<Action>().unwrap();
614        let frames = &action.elements;
615        assert!(frames[0].hit_boxes.is_empty());
616        assert_eq!(frames[0].hurt_boxes.len(), 2);
617        assert_eq!(frames[0].hurt_boxes[0], CollisionBox(15, -1, -24, -72));
618        assert_eq!(frames[0].hurt_boxes[1], CollisionBox(-12, -84, 11, -61));
619        assert!(frames[1].hit_boxes.is_empty());
620        assert!(frames[1].hurt_boxes.is_empty());
621
622        let text = indoc! {"
623            [begin action 001]
624            Clsn1: 2
625             Clsn1[0] =  15, -1,-24,-72
626             Clsn1[1] = -12,-84, 11,-61
627            200, 20, 30, 40, 50
628            200, 30, 30, 40, 50
629        "};
630        let action = text.parse::<Action>().unwrap();
631        let frames = &action.elements;
632        assert!(frames[0].hurt_boxes.is_empty());
633        assert_eq!(frames[0].hit_boxes.len(), 2);
634        assert_eq!(frames[0].hit_boxes[0], CollisionBox(15, -1, -24, -72));
635        assert_eq!(frames[0].hit_boxes[1], CollisionBox(-12, -84, 11, -61));
636        assert!(frames[1].hurt_boxes.is_empty());
637        assert!(frames[1].hit_boxes.is_empty());
638    }
639
640    #[test]
641    fn it_parses_collision_boxes_defaults() {
642        let text = indoc! {"
643            [begin action 001]
644            clsn2default: 1
645             clsn2[0] =  15, -1,-24,-72
646            200, 20, 30, 40, 50
647            200, 40, 30, 40, 50
648        "};
649        let action = text.parse::<Action>().unwrap();
650        let frames = &action.elements;
651        assert!(frames[0].hit_boxes.is_empty());
652        assert_eq!(frames[0].hurt_boxes.len(), 1);
653        assert_eq!(frames[0].hurt_boxes[0], CollisionBox(15, -1, -24, -72));
654        assert!(frames[1].hit_boxes.is_empty());
655        assert_eq!(frames[1].hurt_boxes[0], CollisionBox(15, -1, -24, -72));
656
657        let text = indoc! {"
658            [begin action 001]
659            Clsn1default: 1
660             Clsn1[0] =  15, -1,-24,-72
661            200, 20, 30, 40, 50
662            200, 40, 30, 40, 50
663        "};
664        let action = text.parse::<Action>().unwrap();
665        let frames = &action.elements;
666        assert!(frames[0].hurt_boxes.is_empty());
667        assert_eq!(frames[0].hit_boxes.len(), 1);
668        assert_eq!(frames[0].hit_boxes[0], CollisionBox(15, -1, -24, -72));
669        assert!(frames[1].hurt_boxes.is_empty());
670        assert_eq!(frames[1].hit_boxes[0], CollisionBox(15, -1, -24, -72));
671    }
672
673    #[test]
674    fn it_rejects_collision_boxes_with_unmatched_kind() {
675        let text = indoc! {"
676            [begin action 001]
677            Clsn2: 1
678             clsn1[0] =  15, -1,-24,-72
679            200, 20, 30, 40, 50
680        "};
681        assert!(text.parse::<Action>().is_err());
682
683        let text = indoc! {"
684            [begin action 001]
685            Clsn1: 1
686             clsn2[0] =  15, -1,-24,-72
687            200, 20, 30, 40, 50
688        "};
689        assert!(text.parse::<Action>().is_err());
690    }
691
692    #[test]
693    fn it_rejects_collision_boxes_with_unmatched_count() {
694        let text = indoc! {"
695            [begin action 001]
696            Clsn2: 2
697             clsn2[0] =  15, -1,-24,-72
698            200, 20, 30, 40, 50
699        "};
700        assert!(text.parse::<Action>().is_err());
701
702        let text = indoc! {"
703            [begin action 001]
704            Clsn1: 2
705             clsn1[0] =  15, -1,-24,-72
706            200, 20, 30, 40, 50
707        "};
708        assert!(text.parse::<Action>().is_err());
709    }
710    #[test]
711    fn it_ignores_comments() {
712        let text = indoc! {"
713            [begin action 001] ; Comment
714            200, 20, 30, 40, 50 ; Comment
715            200, 20, 30, 40, 50; Comment
716        "};
717        assert!(text.parse::<Action>().is_ok());
718        let action = text.parse::<Action>().unwrap();
719        assert_eq!(action.elements.len(), 2);
720
721        let text = indoc! {"
722            [begin action 001]
723            200, 20, 30, 40, 50 ; Comment
724        "};
725        assert!(text.parse::<Action>().is_ok());
726        let action = text.parse::<Action>().unwrap();
727        assert_eq!(action.elements.len(), 1);
728
729        let text = indoc! {"
730            [begin action 001]
731            ; Comment
732            ; Comment
733
734            ; Comment
735            200, 20, 30, 40, 50
736        "};
737        assert!(text.parse::<Action>().is_ok());
738        let action = text.parse::<Action>().unwrap();
739        assert_eq!(action.elements.len(), 1);
740
741        let text = indoc! {"
742            [begin action 001]
743            ; Comment
744            ; Comment
745            ; Comment
746            Clsn1default: 1; Comment
747            ; Comment
748            ; Comment
749
750            ; Comment
751             Clsn1[0] =  15, -1,-24,-72 ; Comment
752            ; Comment
753            ; Comment
754            200, 20, 30, 40, 50
755            Clsn1: 2 ; Comment
756             Clsn1[0] =  16, -1,-24,-72 ; Comment
757            ; Comment
758
759            ; Comment
760
761             Clsn1[1] = -12,-84, 11,-61 ; Comment
762            ; Comment
763            200, 40, 30, 40, 50
764        "};
765        assert!(text.parse::<Action>().is_ok());
766        let action = text.parse::<Action>().unwrap();
767        assert_eq!(
768            action.elements[0].hit_boxes[0],
769            CollisionBox(15, -1, -24, -72)
770        );
771        assert_eq!(
772            action.elements[1].hit_boxes[0],
773            CollisionBox(16, -1, -24, -72)
774        );
775        assert_eq!(
776            action.elements[1].hit_boxes[1],
777            CollisionBox(-12, -84, 11, -61)
778        );
779    }
780}