msg_tool\scripts\qlie/
script.rs

1//! Qlie Engine Scenario script (.s)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::*;
6use anyhow::Result;
7use std::io::{Read, Seek, Write};
8
9#[derive(Debug)]
10/// Qlie Engine Scenario script builder
11pub struct QlieScriptBuilder {}
12
13impl QlieScriptBuilder {
14    /// Create a new QlieScriptBuilder
15    pub fn new() -> Self {
16        Self {}
17    }
18}
19
20impl ScriptBuilder for QlieScriptBuilder {
21    fn default_encoding(&self) -> Encoding {
22        Encoding::Cp932
23    }
24
25    fn build_script(
26        &self,
27        buf: Vec<u8>,
28        _filename: &str,
29        encoding: Encoding,
30        _archive_encoding: Encoding,
31        config: &ExtraConfig,
32        _archive: Option<&Box<dyn Script>>,
33    ) -> Result<Box<dyn Script>> {
34        Ok(Box::new(QlieScript::new(
35            MemReader::new(buf),
36            encoding,
37            config,
38        )?))
39    }
40
41    fn extensions(&self) -> &'static [&'static str] {
42        &["s"]
43    }
44
45    fn script_type(&self) -> &'static ScriptType {
46        &ScriptType::Qlie
47    }
48
49    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
50        if is_this_format(buf, buf_len) {
51            Some(20)
52        } else {
53            None
54        }
55    }
56}
57
58/// Check if the buffer is in Qlie script format
59pub fn is_this_format(buf: &[u8], buf_len: usize) -> bool {
60    if buf_len < 2 {
61        return false;
62    }
63    let mut reader = MemReaderRef::new(&buf[..buf_len]);
64    let mut parser = match QlieParser::new(&mut reader, Encoding::Utf8) {
65        Ok(p) => p,
66        Err(_) => return false,
67    };
68    loop {
69        let line = match parser.next_line() {
70            Ok(Some(l)) => l,
71            Ok(None) => break,
72            Err(_) => return false,
73        };
74        let line = line.trim();
75        let line = line.to_lowercase();
76        if line == "@@@avg\\header.s" && line == "@@@avg2\\header.s" {
77            return true;
78        }
79    }
80    return false;
81}
82
83#[derive(Debug, Clone)]
84enum TagData {
85    Simple(String),
86    KeyValue(String, String),
87}
88
89#[derive(Debug, Clone)]
90struct Tag {
91    name: String,
92    args: Vec<TagData>,
93}
94
95impl Tag {
96    fn from_str(s: &str) -> Result<Self> {
97        let mut current = String::new();
98        let mut name = None;
99        let mut arg_key = None;
100        let mut args = Vec::new();
101        let mut in_quote = false;
102        for c in s.chars() {
103            if !in_quote && c == ':' {
104                if name.is_none() {
105                    return Err(anyhow::anyhow!("Invalid tag name: {}", s));
106                }
107                arg_key = Some(current.to_string());
108                current.clear();
109                continue;
110            }
111            if !in_quote && c == ',' {
112                if let Some(key) = arg_key.take() {
113                    args.push(TagData::KeyValue(key, current.to_string()));
114                } else if !current.is_empty() {
115                    if name.is_none() {
116                        name = Some(current.to_string());
117                    } else {
118                        args.push(TagData::Simple(current.to_string()));
119                    }
120                }
121                current.clear();
122                continue;
123            }
124            if c == '"' {
125                in_quote = !in_quote;
126                continue;
127            }
128            current.push(c);
129        }
130        if !current.is_empty() {
131            if let Some(key) = arg_key.take() {
132                args.push(TagData::KeyValue(key, current.to_string()));
133            } else {
134                if name.is_none() {
135                    name = Some(current.to_string());
136                } else {
137                    args.push(TagData::Simple(current.to_string()));
138                }
139            }
140        }
141        Ok(Self {
142            name: name.ok_or(anyhow::anyhow!("Invalid tag name"))?,
143            args,
144        })
145    }
146
147    fn dump(&self) -> String {
148        let mut parts = Vec::new();
149        parts.push(self.name.clone());
150        for arg in &self.args {
151            match arg {
152                TagData::Simple(s) => {
153                    if s.contains(',') || s.contains(':') {
154                        parts.push(format!("\"{}\"", s));
155                    } else {
156                        parts.push(s.clone());
157                    }
158                }
159                TagData::KeyValue(k, v) => {
160                    let v_str = if v.contains(',') || v.contains(':') {
161                        format!("\"{}\"", v)
162                    } else {
163                        v.clone()
164                    };
165                    parts.push(format!("{}:{}", k, v_str));
166                }
167            }
168        }
169        parts.join(",")
170    }
171}
172
173#[derive(Debug, Clone)]
174enum QlieParsedLine {
175    /// `@@label`
176    Label(String),
177    /// `@@@path`
178    Include(String),
179    /// `^tag,attr,...`
180    LineTag(Tag),
181    /// `\command,args,...`
182    Command(Tag),
183    /// `【name】`
184    Name(String),
185    /// `%sound`
186    Sound(String),
187    /// Normal text line
188    Text(String),
189    /// Empty line
190    Empty,
191}
192
193struct QlieParser<T> {
194    reader: T,
195    encoding: Encoding,
196    bom: BomType,
197    parsed: Vec<QlieParsedLine>,
198    is_crlf: bool,
199}
200
201impl<T: Read + Seek> QlieParser<T> {
202    pub fn new(mut reader: T, mut encoding: Encoding) -> Result<Self> {
203        let mut bom = [0; 3];
204        let valid_len = reader.peek(&mut bom)?;
205        let bom = if valid_len >= 2 {
206            if bom[0] == 0xFF && bom[1] == 0xFE {
207                BomType::Utf16LE
208            } else if bom[0] == 0xFE && bom[1] == 0xFF {
209                BomType::Utf16BE
210            } else if valid_len >= 3 && bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF {
211                BomType::Utf8
212            } else {
213                BomType::None
214            }
215        } else {
216            BomType::None
217        };
218        match bom {
219            BomType::Utf16LE => {
220                encoding = Encoding::Utf16LE;
221                reader.seek_relative(2)?;
222            }
223            BomType::Utf16BE => {
224                encoding = Encoding::Utf16BE;
225                reader.seek_relative(2)?;
226            }
227            BomType::Utf8 => {
228                encoding = Encoding::Utf8;
229                reader.seek_relative(3)?;
230            }
231            BomType::None => {}
232        }
233        Ok(Self {
234            reader,
235            encoding,
236            bom,
237            parsed: Vec::new(),
238            is_crlf: false,
239        })
240    }
241
242    fn next_line(&mut self) -> Result<Option<String>> {
243        let mut sbuf = Vec::new();
244        let mut is_eof = false;
245        if self.encoding.is_utf16le() {
246            let mut buf = [0; 2];
247            loop {
248                let readed = self.reader.read(&mut buf)?;
249                if readed == 0 {
250                    is_eof = true;
251                    break;
252                }
253                if buf == [0x0A, 0x00] {
254                    break;
255                }
256                sbuf.extend_from_slice(&buf);
257            }
258        } else if self.encoding.is_utf16be() {
259            let mut buf = [0; 2];
260            loop {
261                let readed = self.reader.read(&mut buf)?;
262                if readed == 0 {
263                    is_eof = true;
264                    break;
265                }
266                if buf == [0x00, 0x0A] {
267                    break;
268                }
269                sbuf.extend_from_slice(&buf);
270            }
271        } else {
272            let mut buf = [0; 1];
273            loop {
274                let readed = self.reader.read(&mut buf)?;
275                if readed == 0 {
276                    is_eof = true;
277                    break;
278                }
279                if buf[0] == 0x0A {
280                    break;
281                }
282                sbuf.push(buf[0]);
283            }
284        }
285        if sbuf.is_empty() {
286            return Ok(if is_eof { None } else { Some(String::new()) });
287        }
288        let mut s = decode_to_string(self.encoding, &sbuf, true)?;
289        if s.ends_with("\r") {
290            s.pop();
291            self.is_crlf = true;
292        }
293        Ok(Some(s))
294    }
295
296    pub fn parse(&mut self) -> Result<()> {
297        while let Some(line) = self.next_line()? {
298            let line = line.trim();
299            if line.is_empty() {
300                self.parsed.push(QlieParsedLine::Empty);
301            } else if line.starts_with("@@@") {
302                self.parsed
303                    .push(QlieParsedLine::Include(line[3..].to_string()));
304            } else if line.starts_with("@@") {
305                self.parsed
306                    .push(QlieParsedLine::Label(line[2..].to_string()));
307            } else if line.starts_with("^") {
308                let tag = Tag::from_str(&line[1..])?;
309                self.parsed.push(QlieParsedLine::LineTag(tag));
310            } else if line.starts_with("\\") {
311                let tag = Tag::from_str(&line[1..])?;
312                self.parsed.push(QlieParsedLine::Command(tag));
313            } else if line.starts_with("【") && line.ends_with("】") {
314                let name = line[3..line.len() - 3].to_string();
315                self.parsed.push(QlieParsedLine::Name(name));
316            } else if line.starts_with("%") {
317                let sound = line[3..].to_string();
318                self.parsed.push(QlieParsedLine::Sound(sound));
319            } else {
320                self.parsed.push(QlieParsedLine::Text(line.to_string()));
321            }
322        }
323        Ok(())
324    }
325}
326
327#[derive(Debug)]
328struct QlieDumper<T: Write> {
329    writer: T,
330    encoding: Encoding,
331    is_crlf: bool,
332}
333
334impl<T: Write> QlieDumper<T> {
335    pub fn new(mut writer: T, bom: BomType, mut encoding: Encoding, is_crlf: bool) -> Result<Self> {
336        match bom {
337            BomType::Utf16LE => {
338                encoding = Encoding::Utf16LE;
339            }
340            BomType::Utf16BE => {
341                encoding = Encoding::Utf16BE;
342            }
343            BomType::Utf8 => {
344                encoding = Encoding::Utf8;
345            }
346            BomType::None => {}
347        }
348        writer.write_all(bom.as_bytes())?;
349        Ok(Self {
350            writer,
351            encoding,
352            is_crlf,
353        })
354    }
355
356    fn write_line(&mut self, line: &str) -> Result<()> {
357        let line = if self.is_crlf {
358            format!("{}\r\n", line)
359        } else {
360            format!("{}\n", line)
361        };
362        let data = encode_string(self.encoding, &line, false)?;
363        self.writer.write_all(&data)?;
364        Ok(())
365    }
366
367    pub fn dump(mut self, data: &[QlieParsedLine]) -> Result<()> {
368        for line in data {
369            match line {
370                QlieParsedLine::Label(s) => {
371                    self.write_line(&format!("@@{}", s))?;
372                }
373                QlieParsedLine::Include(s) => {
374                    self.write_line(&format!("@@@{}", s))?;
375                }
376                QlieParsedLine::LineTag(tag) => {
377                    self.write_line(&format!("^{}", tag.dump()))?;
378                }
379                QlieParsedLine::Command(cmd) => {
380                    self.write_line(&format!("\\{}", cmd.dump()))?;
381                }
382                QlieParsedLine::Name(name) => {
383                    self.write_line(&format!("【{}】", name))?;
384                }
385                QlieParsedLine::Sound(sound) => {
386                    self.write_line(&format!("%{}", sound))?;
387                }
388                QlieParsedLine::Text(text) => {
389                    self.write_line(text)?;
390                }
391                QlieParsedLine::Empty => {
392                    self.write_line("")?;
393                }
394            }
395        }
396        Ok(())
397    }
398}
399
400#[derive(Debug)]
401pub struct QlieScript {
402    bom: BomType,
403    parsed: Vec<QlieParsedLine>,
404    is_crlf: bool,
405}
406
407impl QlieScript {
408    /// Create a new QlieScript
409    pub fn new<T: Read + Seek>(data: T, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
410        let mut parser = QlieParser::new(data, encoding)?;
411        parser.parse()?;
412        Ok(Self {
413            bom: parser.bom,
414            parsed: parser.parsed,
415            is_crlf: parser.is_crlf,
416        })
417    }
418}
419
420impl Script for QlieScript {
421    fn default_output_script_type(&self) -> OutputScriptType {
422        OutputScriptType::Json
423    }
424
425    fn default_format_type(&self) -> FormatOptions {
426        FormatOptions::None
427    }
428
429    fn extract_messages(&self) -> Result<Vec<Message>> {
430        let mut messages = Vec::new();
431        let mut name = None;
432        for line in &self.parsed {
433            match line {
434                QlieParsedLine::Name(n) => {
435                    name = Some(n.to_string());
436                }
437                QlieParsedLine::Text(text) => {
438                    messages.push(Message::new(text.replace("[n]", "\n"), name.take()));
439                }
440                QlieParsedLine::LineTag(tag) => {
441                    if tag.name.to_lowercase() == "select" {
442                        for arg in &tag.args {
443                            match arg {
444                                TagData::Simple(s) => {
445                                    messages.push(Message::new(s.clone(), None));
446                                }
447                                _ => {
448                                    return Err(anyhow::anyhow!(
449                                        "Invalid select tag argument: {:?}.",
450                                        tag
451                                    ));
452                                }
453                            }
454                        }
455                    } else if tag.name.to_lowercase() == "savetext" {
456                        if tag.args.len() >= 1 {
457                            match &tag.args[0] {
458                                TagData::Simple(s) => {
459                                    messages.push(Message::new(s.clone(), None));
460                                }
461                                _ => {
462                                    return Err(anyhow::anyhow!(
463                                        "Invalid savetext tag argument: {:?}.",
464                                        tag
465                                    ));
466                                }
467                            }
468                        }
469                    }
470                }
471                _ => {}
472            }
473        }
474        Ok(messages)
475    }
476
477    fn import_messages<'a>(
478        &'a self,
479        messages: Vec<Message>,
480        file: Box<dyn WriteSeek + 'a>,
481        _filename: &str,
482        encoding: Encoding,
483        replacement: Option<&'a ReplacementTable>,
484    ) -> Result<()> {
485        let mut mess = messages.iter();
486        let mut mes = mess.next();
487        let mut lines = self.parsed.clone();
488        let mut name_index = None;
489        let mut index = 0;
490        let line_len = lines.len();
491        while index < line_len {
492            let line = lines[index].clone();
493            match line {
494                QlieParsedLine::Name(_) => {
495                    name_index = Some(index);
496                }
497                QlieParsedLine::LineTag(tag) => {
498                    if tag.name.to_lowercase() == "select" {
499                        let mut new_tag = Tag {
500                            name: tag.name.clone(),
501                            args: Vec::new(),
502                        };
503                        for _ in &tag.args {
504                            let mut message = match mes {
505                                Some(m) => m.message.clone(),
506                                None => {
507                                    return Err(anyhow::anyhow!("Not enough messages to import."));
508                                }
509                            };
510                            mes = mess.next();
511                            if let Some(repl) = replacement {
512                                for (k, v) in &repl.map {
513                                    message = message.replace(k, v);
514                                }
515                            }
516                            new_tag.args.push(TagData::Simple(message));
517                        }
518                        lines[index] = QlieParsedLine::LineTag(new_tag);
519                    } else if tag.name.to_lowercase() == "savetext" {
520                        if tag.args.len() >= 1 {
521                            let mut message = match mes {
522                                Some(m) => m.message.clone(),
523                                None => {
524                                    return Err(anyhow::anyhow!("Not enough messages to import."));
525                                }
526                            };
527                            mes = mess.next();
528                            if let Some(repl) = replacement {
529                                for (k, v) in &repl.map {
530                                    message = message.replace(k, v);
531                                }
532                            }
533                            let new_tag = Tag {
534                                name: tag.name.clone(),
535                                args: vec![TagData::Simple(message)],
536                            };
537                            lines[index] = QlieParsedLine::LineTag(new_tag);
538                        }
539                    }
540                }
541                QlieParsedLine::Text(_) => {
542                    if let Some(name_index) = name_index.take() {
543                        let mut name = match mes {
544                            Some(m) => match &m.name {
545                                Some(n) => n.clone(),
546                                None => return Err(anyhow::anyhow!("Expected name for message.")),
547                            },
548                            None => return Err(anyhow::anyhow!("Not enough messages to import.")),
549                        };
550                        if let Some(repl) = replacement {
551                            for (k, v) in &repl.map {
552                                name = name.replace(k, v);
553                            }
554                        }
555                        lines[name_index] = QlieParsedLine::Name(name);
556                    }
557                    let mut message = match mes {
558                        Some(m) => m.message.clone(),
559                        None => return Err(anyhow::anyhow!("Not enough messages to import.")),
560                    };
561                    mes = mess.next();
562                    if let Some(repl) = replacement {
563                        for (k, v) in &repl.map {
564                            message = message.replace(k, v);
565                        }
566                    }
567                    lines[index] = QlieParsedLine::Text(message.replace("\n", "[n]"));
568                }
569                _ => {}
570            }
571            index += 1;
572        }
573        let dumper = QlieDumper::new(file, self.bom, encoding, self.is_crlf)?;
574        dumper.dump(&lines)?;
575        Ok(())
576    }
577}
578
579#[test]
580fn test_tag() {
581    let s = "tag1,\"test:a,c\",best:\"va,2:3\"";
582    let parts = Tag::from_str(s).unwrap();
583    assert_eq!(parts.name, "tag1");
584    assert_eq!(parts.args.len(), 2);
585    match &parts.args[0] {
586        TagData::Simple(v) => assert_eq!(v, "test:a,c"),
587        _ => panic!("Expected Simple"),
588    }
589    match &parts.args[1] {
590        TagData::KeyValue(k, v) => {
591            assert_eq!(k, "best");
592            assert_eq!(v, "va,2:3");
593        }
594        _ => panic!("Expected KeyValue"),
595    }
596    let dumped = parts.dump();
597    assert_eq!(dumped, s);
598}