1use crate::scripts::base::*;
3use crate::types::*;
4use crate::utils::encoding::*;
5use anyhow::Result;
6use std::collections::HashMap;
7use std::ops::{Deref, DerefMut};
8use std::sync::Arc;
9use unicode_segmentation::UnicodeSegmentation;
10
11#[derive(Debug)]
12pub struct YurisTxtBuilder {}
13
14impl YurisTxtBuilder {
15 pub const fn new() -> Self {
17 YurisTxtBuilder {}
18 }
19}
20
21impl ScriptBuilder for YurisTxtBuilder {
22 fn default_encoding(&self) -> Encoding {
23 Encoding::Cp932
24 }
25
26 fn build_script(
27 &self,
28 buf: Vec<u8>,
29 _filename: &str,
30 encoding: Encoding,
31 _archive_encoding: Encoding,
32 config: &ExtraConfig,
33 _archive: Option<&Box<dyn Script>>,
34 ) -> Result<Box<dyn Script + Send + Sync>> {
35 Ok(Box::new(YurisTxt::new(&buf, encoding, config)?))
36 }
37
38 fn extensions(&self) -> &'static [&'static str] {
39 &["txt"]
40 }
41
42 fn script_type(&self) -> &'static ScriptType {
43 &ScriptType::YurisTxt
44 }
45}
46
47trait INode {
48 fn serialize(&self) -> String;
49}
50
51#[derive(Clone, Debug, PartialEq)]
52struct CommentNode(String);
53
54impl Deref for CommentNode {
55 type Target = String;
56
57 fn deref(&self) -> &Self::Target {
58 &self.0
59 }
60}
61
62impl DerefMut for CommentNode {
63 fn deref_mut(&mut self) -> &mut Self::Target {
64 &mut self.0
65 }
66}
67
68impl INode for CommentNode {
69 fn serialize(&self) -> String {
70 format!("//{}", self.0)
71 }
72}
73
74#[derive(Clone, Debug, PartialEq)]
75struct LabelNode(String);
76
77impl Deref for LabelNode {
78 type Target = String;
79
80 fn deref(&self) -> &Self::Target {
81 &self.0
82 }
83}
84
85impl DerefMut for LabelNode {
86 fn deref_mut(&mut self) -> &mut Self::Target {
87 &mut self.0
88 }
89}
90
91impl INode for LabelNode {
92 fn serialize(&self) -> String {
93 format!("#{}", self.0)
94 }
95}
96
97#[derive(Clone, Debug, PartialEq)]
98struct CommandNode {
99 name: String,
100 args: Vec<String>,
101 has_args: bool,
102}
103
104impl INode for CommandNode {
105 fn serialize(&self) -> String {
106 if !self.has_args {
107 return format!("\\{}", self.name);
108 }
109 let mut s = format!("\\{}(", self.name);
110 let mut first = true;
111 for arg in &self.args {
112 if first {
113 first = false;
114 } else {
115 s.push_str(", ");
116 }
117 if (arg.contains(" ") && !arg.chars().all(|s| s.is_ascii_digit() || s == ' '))
118 || arg.contains(",")
119 || arg.contains("=")
120 {
121 s.push_str(&format!("\"{}\"", arg));
122 } else {
123 s.push_str(arg);
124 }
125 }
126 s.push(')');
127 s
128 }
129}
130
131#[derive(Clone, Debug, PartialEq)]
132struct NameNode(String);
133
134impl Deref for NameNode {
135 type Target = String;
136 fn deref(&self) -> &Self::Target {
137 &self.0
138 }
139}
140
141impl DerefMut for NameNode {
142 fn deref_mut(&mut self) -> &mut Self::Target {
143 &mut self.0
144 }
145}
146
147impl INode for NameNode {
148 fn serialize(&self) -> String {
149 format!("【{}】", self.0)
150 }
151}
152
153#[derive(Clone, Debug, PartialEq)]
154struct TextNode(String);
155
156impl Deref for TextNode {
157 type Target = String;
158 fn deref(&self) -> &Self::Target {
159 &self.0
160 }
161}
162
163impl DerefMut for TextNode {
164 fn deref_mut(&mut self) -> &mut Self::Target {
165 &mut self.0
166 }
167}
168
169impl INode for TextNode {
170 fn serialize(&self) -> String {
171 self.0.clone()
172 }
173}
174
175#[derive(Clone, Debug, PartialEq)]
176struct CommentBlock(String);
177
178impl Deref for CommentBlock {
179 type Target = String;
180 fn deref(&self) -> &Self::Target {
181 &self.0
182 }
183}
184
185impl DerefMut for CommentBlock {
186 fn deref_mut(&mut self) -> &mut Self::Target {
187 &mut self.0
188 }
189}
190
191impl INode for CommentBlock {
192 fn serialize(&self) -> String {
193 format!("/*{}*/", self.0)
194 }
195}
196
197#[derive(Clone, Debug, PartialEq)]
198enum LineNode {
199 Comment(CommentNode),
200 Comments(CommentBlock),
201 Command(CommandNode),
202 Name(NameNode),
203 Text(TextNode),
204}
205
206impl INode for LineNode {
207 fn serialize(&self) -> String {
208 match self {
209 Self::Comment(node) => node.serialize(),
210 Self::Comments(node) => node.serialize(),
211 Self::Command(node) => node.serialize(),
212 Self::Name(node) => node.serialize(),
213 Self::Text(node) => node.serialize(),
214 }
215 }
216}
217
218impl INode for Vec<LineNode> {
219 fn serialize(&self) -> String {
220 self.iter()
221 .map(|s| s.serialize())
222 .collect::<Vec<_>>()
223 .join("")
224 }
225}
226
227#[derive(Clone, Debug, PartialEq)]
228enum Line {
229 Line(Vec<LineNode>),
230 Empty,
231 Label(LabelNode),
232}
233
234impl INode for Line {
235 fn serialize(&self) -> String {
236 match self {
237 Self::Line(line) => line.serialize(),
238 Self::Empty => String::new(),
239 Self::Label(label) => label.serialize(),
240 }
241 }
242}
243
244impl INode for Vec<Line> {
245 fn serialize(&self) -> String {
246 self.iter()
247 .map(|s| s.serialize())
248 .collect::<Vec<_>>()
249 .join("\n")
250 }
251}
252
253#[derive(Debug)]
254struct Parser<'a> {
255 lines: std::str::Lines<'a>,
256 cur_line: &'a str,
257 cur_pos: usize,
258 line_num: u64,
259 cur_line_chars: Vec<&'a str>,
260}
261
262impl<'a> Parser<'a> {
263 fn new(data: &'a str) -> Self {
264 Self {
265 lines: data.lines(),
266 cur_line: "",
267 cur_pos: 0,
268 line_num: 0,
269 cur_line_chars: Vec::new(),
270 }
271 }
272
273 fn error(&self, msg: impl std::fmt::Display) -> anyhow::Error {
274 anyhow::anyhow!("{} at line {} char {}", msg, self.line_num, self.cur_pos)
275 }
276
277 fn parse(mut self) -> Result<Vec<Line>> {
278 let mut lines = Vec::new();
279 while let Some(line) = self.lines.next() {
280 self.line_num += 1;
281 self.cur_line = line;
282 self.cur_line_chars = line.graphemes(true).collect();
283 lines.push(self.parse_line()?);
284 }
285 Ok(lines)
286 }
287
288 fn add_next_line(&mut self) -> Result<()> {
289 let next_line = self
290 .lines
291 .next()
292 .ok_or_else(|| anyhow::anyhow!("Unexpected eof"))?;
293 self.line_num += 1;
294 self.cur_line = next_line;
295 self.cur_line_chars.push("\n");
296 self.cur_line_chars.extend(next_line.graphemes(true));
297 Ok(())
298 }
299
300 fn parse_line(&mut self) -> Result<Line> {
301 self.cur_pos = 0;
302 if self.cur_line.trim_matches(' ').is_empty() {
303 return Ok(Line::Empty);
304 }
305 let mut line = Vec::new();
306 let mut text = String::new();
307 while let Some(c) = self.peek_char() {
308 if text.is_empty() && (c == " " || c == "\t") {
310 self.cur_pos += 1;
311 continue;
312 }
313 if line.is_empty() && c == "#" {
315 self.cur_pos += 1;
316 let label = self.cur_line_chars[self.cur_pos..].join("");
317 return Ok(Line::Label(LabelNode(label)));
318 }
319 if c == "/" {
320 if let Some(c) = self.peek_char_offset(1) {
322 if c == "/" {
323 let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
324 if !ctext.is_empty() {
325 line.push(LineNode::Text(TextNode(ctext.to_owned())));
326 text.clear();
327 }
328 self.cur_pos += 2;
329 let comment = self.cur_line_chars[self.cur_pos..].join("");
330 line.push(LineNode::Comment(CommentNode(comment)));
331 break;
332 } else if c == "*" {
333 let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
334 if !ctext.is_empty() {
335 line.push(LineNode::Text(TextNode(ctext.to_owned())));
336 text.clear();
337 }
338 self.cur_pos += 2;
339 let start_pos = self.cur_pos;
340 let mut ok = false;
341 loop {
342 while let Some(c) = self.next_char() {
343 if c == "*" && self.peek_char().is_some_and(|c| c == "/") {
344 let end_pos = self.cur_pos - 1;
345 self.cur_pos += 1;
346 ok = true;
347 line.push(LineNode::Comments(CommentBlock(
348 self.cur_line_chars[start_pos..end_pos].join(""),
349 )));
350 break;
351 }
352 }
353 if ok {
354 break;
355 }
356 self.add_next_line()?;
357 }
358 continue;
359 }
360 }
361 }
362 if c == "\\" {
364 let cmd = self.parse_command()?;
365 if !cmd.has_args && cmd.name == "R" {
366 text.push_str("\\R");
367 continue;
368 }
369 let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
370 if !ctext.is_empty() {
371 line.push(LineNode::Text(TextNode(ctext.to_owned())));
372 text.clear();
373 }
374 line.push(LineNode::Command(cmd));
375 continue;
376 }
377 if c == "【" {
379 let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
380 if !ctext.is_empty() {
381 line.push(LineNode::Text(TextNode(ctext.to_owned())));
382 text.clear();
383 }
384 line.push(LineNode::Name(self.parse_name()?));
385 continue;
386 }
387 text.push_str(c);
388 self.cur_pos += 1;
389 }
390 let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
391 if !ctext.is_empty() {
392 line.push(LineNode::Text(TextNode(ctext.to_owned())));
393 }
394 Ok(Line::Line(line))
395 }
396
397 fn parse_command(&mut self) -> Result<CommandNode> {
398 let c = self
399 .next_char_with_line()
400 .ok_or_else(|| self.error("Unexpected end of file"))?;
401 if c != "\\" {
402 return Err(self.error("Unexpected command start token"));
403 }
404 let mut name = String::new();
405 let mut args = Vec::new();
406 let mut in_quote = false;
407 let mut arg = String::new();
408 let mut ok = false;
409 while let Some(c) = self.peek_char() {
410 if c == "(" {
411 ok = true;
412 self.cur_pos += 1;
413 break;
414 }
415 if c == ")" {
416 return Err(self.error("Unexpected ) when parsing command"));
417 }
418 if !c.is_ascii() {
419 break;
420 }
421 name.push_str(c);
422 self.cur_pos += 1;
423 continue;
424 }
425 if !ok {
426 return Ok(CommandNode {
427 name: name.trim_matches(' ').trim_matches('\t').to_owned(),
428 args: Vec::new(),
429 has_args: false,
430 });
431 }
432 loop {
433 let c = self
434 .next_char_with_line()
435 .ok_or_else(|| self.error("Unexpected end of file when parsing command"))?;
436 if in_quote {
437 if c == "\"" {
438 in_quote = false;
439 continue;
440 }
441 } else {
442 if c == "\"" {
443 in_quote = true;
444 continue;
445 }
446 if c == "\n" || c == "\r\n" {
447 continue;
448 }
449 if c == " " || c == "\t" {
450 if arg.is_empty() {
451 continue;
452 }
453 let mut tmp = c.to_string();
454 while let Some(c) = self.peek_char() {
455 if c == " " || c == "\t" {
456 self.cur_pos += 1;
457 tmp.push_str(c);
458 } else if c == "," || c == ")" {
459 break;
460 } else {
461 arg.push_str(&tmp);
462 break;
463 }
464 }
465 continue;
466 }
467 if c == "\\" {
468 args.push(arg);
469 self.cur_pos -= 1;
470 return Ok(CommandNode {
471 name: name.trim_matches(' ').trim_matches('\t').to_owned(),
472 args,
473 has_args: true,
474 });
475 }
476 if c == "," {
477 args.push(arg);
478 arg = String::new();
479 continue;
480 }
481 if c == ")" {
482 args.push(arg);
483 return Ok(CommandNode {
484 name: name.trim_matches(' ').trim_matches('\t').to_owned(),
485 args,
486 has_args: true,
487 });
488 }
489 }
490 arg.push_str(c);
491 }
492 }
493
494 fn parse_name(&mut self) -> Result<NameNode> {
495 let c = self
496 .next_char()
497 .ok_or_else(|| self.error("Unexpected end of line"))?;
498 if c != "【" {
499 return Err(self.error("Unexpected command start token"));
500 }
501 let mut name = String::new();
502 loop {
503 let c = self
504 .next_char()
505 .ok_or_else(|| self.error("Unexpected end of line when parsing name"))?;
506 if c == "】" {
507 return Ok(NameNode(name));
508 }
509 name.push_str(c);
510 }
511 }
512
513 fn peek_char(&self) -> Option<&'a str> {
514 self.cur_line_chars.get(self.cur_pos).map(|s| *s)
515 }
516
517 fn peek_char_offset(&self, offset: isize) -> Option<&'a str> {
518 let target = (self.cur_pos as isize + offset as isize) as usize;
519 self.cur_line_chars.get(target).map(|s| *s)
520 }
521
522 fn next_char(&mut self) -> Option<&'a str> {
523 let t = self.cur_line_chars.get(self.cur_pos).map(|s| *s);
524 if t.is_some() {
525 self.cur_pos += 1;
526 }
527 t
528 }
529
530 fn next_char_with_line(&mut self) -> Option<&'a str> {
531 let t = self.cur_line_chars.get(self.cur_pos).map(|s| *s);
532 if t.is_some() {
533 self.cur_pos += 1;
534 return t;
535 }
536 if self.add_next_line().is_err() {
537 return None;
538 }
539 self.next_char()
540 }
541}
542
543#[derive(Debug)]
544pub struct YurisTxt {
545 data: Vec<Line>,
546 bom: BomType,
547 tips_map: Option<Arc<HashMap<String, String>>>,
548}
549
550impl YurisTxt {
551 pub fn new<D: AsRef<[u8]> + ?Sized>(
552 data: &D,
553 encoding: Encoding,
554 config: &ExtraConfig,
555 ) -> Result<Self> {
556 let (text, bom) = decode_with_bom_detect(encoding, data.as_ref(), true)?;
557 let data = Parser::new(&text).parse()?;
558 Ok(Self {
559 data,
560 bom,
561 tips_map: config.yuris_tips_map.clone(),
562 })
563 }
564}
565
566impl Script for YurisTxt {
567 fn default_output_script_type(&self) -> OutputScriptType {
568 OutputScriptType::Json
569 }
570
571 fn default_format_type(&self) -> FormatOptions {
572 FormatOptions::None
573 }
574
575 fn extract_messages(&self) -> Result<Vec<Message>> {
576 let mut messages = Vec::new();
577 for line in &self.data {
578 if let Line::Line(line) = line {
579 let mut name = None;
580 let mut message = String::new();
581 for node in line.iter() {
582 if let LineNode::Name(n) = node {
583 name = Some(n.as_str());
584 } else if let LineNode::Text(text) = node {
585 message.push_str(&text.replace("\\R", "\n"));
586 } else if let LineNode::Command(cmd) = node {
587 if !message.is_empty() {
588 message.push_str(&cmd.serialize());
589 }
590 if cmd.name == "SEL" {
591 for arg in &cmd.args {
592 messages.push(Message::new(arg.to_owned(), None));
593 }
594 }
595 }
596 }
597 if !message.is_empty() {
598 messages.push(Message::new(message, name.map(|s| s.to_owned())));
599 }
600 }
601 }
602 Ok(messages)
603 }
604
605 fn import_messages<'a>(
606 &'a self,
607 messages: Vec<Message>,
608 mut file: Box<dyn WriteSeek + 'a>,
609 _filename: &str,
610 encoding: Encoding,
611 replacement: Option<&'a ReplacementTable>,
612 ) -> Result<()> {
613 let mut data = self.data.clone();
614 let mut mess = messages.iter();
615 let mut mes = mess.next();
616 for line in data.iter_mut() {
617 if let Line::Line(line) = line {
618 let mut name_index = None;
619 let mut message_index = None;
620 for (i, node) in line.iter_mut().enumerate() {
621 if let LineNode::Name(_) = node {
622 name_index = Some(i);
623 } else if let LineNode::Text(_) = node {
624 if message_index.is_none() {
625 message_index = Some(i);
626 }
627 } else if let LineNode::Command(cmd) = node {
628 if cmd.name == "SEL" {
629 for arg in cmd.args.iter_mut() {
630 let mut m = mes
631 .ok_or_else(|| anyhow::anyhow!("No more messages to import"))?
632 .message
633 .clone();
634 mes = mess.next();
635 if let Some(rep) = replacement {
636 for (k, v) in &rep.map {
637 m = m.replace(k, v);
638 }
639 }
640 *arg = m;
641 }
642 } else if cmd.name == "TIPS.SET" {
643 if cmd.args.len() >= 1 {
644 if let Some(tips) = self.tips_map.as_ref() {
645 if let Some(data) = tips.get(&cmd.args[0]) {
646 let mut m = data.to_owned();
647 if let Some(rep) = replacement {
648 for (k, v) in &rep.map {
649 m = m.replace(k, v);
650 }
651 }
652 cmd.args[0] = m;
653 }
654 }
655 }
656 }
657 }
658 }
659 if let Some(message_idx) = message_index {
660 let m = mes.ok_or_else(|| anyhow::anyhow!("No more messages to import"))?;
661 mes = mess.next();
662 if let Some(name_idx) = name_index {
663 let mut name = m
664 .name
665 .as_ref()
666 .ok_or_else(|| anyhow::anyhow!("Message don't have name"))?
667 .clone();
668 if let Some(rep) = replacement {
669 for (k, v) in &rep.map {
670 name = name.replace(k, v);
671 }
672 }
673 if let LineNode::Name(n) = &mut line[name_idx] {
674 n.0 = name;
675 }
676 }
677 let mut m = m.message.replace("\n", "\\R");
678 if let Some(rep) = replacement {
679 for (k, v) in &rep.map {
680 m = m.replace(k, v);
681 }
682 }
683 let data = Parser::new(&m).parse()?;
684 if data.len() != 1 {
685 anyhow::bail!("parsed length is not 1.");
686 }
687 let li = data[0].clone();
688 match li {
689 Line::Label(_) => anyhow::bail!("Unsupported"),
690 Line::Empty => {
691 line.splice(message_idx.., []);
692 }
693 Line::Line(li) => {
694 line.splice(message_idx.., li);
695 }
696 }
697 }
698 }
699 }
700 if mes.is_some() || mess.next().is_some() {
701 return Err(anyhow::anyhow!("Some messages were not processed."));
702 }
703 let data = data.serialize();
704 let data = encode_string_with_bom(encoding, &data, false, self.bom)?;
705 file.write_all(&data)?;
706 Ok(())
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 #[test]
714 fn test_parse1() {
715 let data = "\\T( , 250 ) \t【name】\t「……なんて」\t";
716 assert_eq!(
717 Parser::new(data).parse().unwrap(),
718 vec![Line::Line(vec![
719 LineNode::Command(CommandNode {
720 name: "T".into(),
721 args: vec!["".into(), "250".into()],
722 has_args: true,
723 }),
724 LineNode::Name(NameNode("name".into())),
725 LineNode::Text(TextNode("「……なんて」".into())),
726 ])]
727 );
728 }
729 #[test]
730 fn test_parse2() {
731 let data = "\\T(2 5 \t0\t ) //TEST\n\\T ( \"250 \" , \"Wor,ks\" )";
732 assert_eq!(
733 Parser::new(data).parse().unwrap(),
734 vec![
735 Line::Line(vec![
736 LineNode::Command(CommandNode {
737 name: "T".into(),
738 args: vec!["2 5 \t0".into()],
739 has_args: true,
740 }),
741 LineNode::Comment(CommentNode("TEST".into()))
742 ]),
743 Line::Line(vec![LineNode::Command(CommandNode {
744 name: "T".into(),
745 args: vec!["250 ".into(), "Wor,ks".into()],
746 has_args: true,
747 }),])
748 ]
749 );
750 }
751 #[test]
752 fn test_parse3() {
753 let data = "\\VO(UDA_0_ALL_0007_0004)【ウダツ】「んで、昨日あの後どうしたん? 実習の日程はもう決まった\\Rのか?」";
754 assert_eq!(
755 Parser::new(data).parse().unwrap(),
756 vec![Line::Line(vec![
757 LineNode::Command(CommandNode {
758 name: "VO".into(),
759 args: vec!["UDA_0_ALL_0007_0004".into()],
760 has_args: true,
761 }),
762 LineNode::Name(NameNode("ウダツ".into())),
763 LineNode::Text(TextNode(
764 "「んで、昨日あの後どうしたん? 実習の日程はもう決まった\\Rのか?」".into()
765 )),
766 ])]
767 );
768 }
769 #[test]
770 fn test_parse4() {
771 let data = "\\GO.TITLE";
772 assert_eq!(
773 Parser::new(data).parse().unwrap(),
774 vec![Line::Line(vec![LineNode::Command(CommandNode {
775 name: "GO.TITLE".into(),
776 args: vec![],
777 has_args: false,
778 }),])]
779 );
780 }
781 #[test]
782 fn test_parse5() {
783 let data = r"TEST/*
784\FOUT(600, 42, white)
785\BG.CMXYZ( 402, 0, -45)
786\BG(bg51 , 260, 0, 0)
787\PSET(回想フレーム, 0)
788\FIN(600, 41)
789*/Test";
790 assert_eq!(
791 Parser::new(data).parse().unwrap(),
792 vec![Line::Line(vec![
793 LineNode::Text(TextNode("TEST".into())),
794 LineNode::Comments(CommentBlock(
795 r"
796\FOUT(600, 42, white)
797\BG.CMXYZ( 402, 0, -45)
798\BG(bg51 , 260, 0, 0)
799\PSET(回想フレーム, 0)
800\FIN(600, 41)
801"
802 .into()
803 )),
804 LineNode::Text(TextNode("Test".into())),
805 ])]
806 );
807 }
808 #[test]
809 fn test_parse6() {
810 let data = r"\S.D(HASIRA
811)";
812 assert_eq!(
813 Parser::new(data).parse().unwrap(),
814 vec![Line::Line(vec![LineNode::Command(CommandNode {
815 name: "S.D".into(),
816 args: vec!["HASIRA".into()],
817 has_args: true
818 })])],
819 );
820 }
821 #[test]
822 fn test_parse7() {
823 let data = r"\S.CLXYZ(d,500,0,-40,0,1,1
824
825\VO(KAG_0_ALL_4010_0016)【カグヤ】「皆さんっ、すぐそうやって! ひとつしかない大切な身体なんですから今日みたいなことは……」";
826 assert_eq!(
827 Parser::new(data).parse().unwrap(),
828 vec![
829 Line::Line(vec![
830 LineNode::Command(CommandNode {
831 name: "S.CLXYZ".into(),
832 args: vec![
833 "d".into(),
834 "500".into(),
835 "0".into(),
836 "-40".into(),
837 "0".into(),
838 "1".into(),
839 "1".into()
840 ],
841 has_args: true
842 }),
843 LineNode::Command(CommandNode {
844 name: "VO".into(),
845 args: vec!["KAG_0_ALL_4010_0016".into()],
846 has_args: true,
847 }),
848 LineNode::Name(NameNode("カグヤ".into())),
849 LineNode::Text(TextNode("「皆さんっ、すぐそうやって! ひとつしかない大切な身体なんですから今日みたいなことは……」".into())),
850 ])
851 ],
852 );
853 }
854 #[test]
855 fn test_parse8() {
856 let data = r"\RP.END(SCENE_ETC_H04)";
857 assert_eq!(
858 Parser::new(data).parse().unwrap(),
859 vec![Line::Line(vec![LineNode::Command(CommandNode {
860 name: "RP.END".into(),
861 args: vec!["SCENE_ETC_H04".into()],
862 has_args: true
863 })])],
864 );
865 }
866 #[test]
867 fn test_ser1() {
868 assert_eq!(
869 (CommandNode {
870 name: "EV.CMXYZ".into(),
871 args: vec!["0 474".into(), "54".into(), "-58".into()],
872 has_args: true,
873 })
874 .serialize(),
875 r"\EV.CMXYZ(0 474, 54, -58)"
876 );
877 }
878}