msg_tool\scripts\cat_system/
cst.rs

1//! CatSystem2 Scene Script File (.cst)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::*;
6use anyhow::Result;
7use fancy_regex::Regex;
8use int_enum::IntEnum;
9use std::io::{Read, Write};
10
11#[derive(Debug)]
12/// Builder for CatSystem2 Scene Script files.
13pub struct CstScriptBuilder {}
14
15impl CstScriptBuilder {
16    /// Creates a new instance of `CstScriptBuilder`.
17    pub fn new() -> Self {
18        CstScriptBuilder {}
19    }
20}
21
22impl ScriptBuilder for CstScriptBuilder {
23    fn default_encoding(&self) -> Encoding {
24        Encoding::Cp932
25    }
26
27    fn build_script(
28        &self,
29        buf: Vec<u8>,
30        _filename: &str,
31        encoding: Encoding,
32        _archive_encoding: Encoding,
33        config: &ExtraConfig,
34        _archive: Option<&Box<dyn Script>>,
35    ) -> Result<Box<dyn Script>> {
36        Ok(Box::new(CstScript::new(buf, encoding, config)?))
37    }
38
39    fn extensions(&self) -> &'static [&'static str] {
40        &["cst"]
41    }
42
43    fn script_type(&self) -> &'static ScriptType {
44        &ScriptType::CatSystem
45    }
46
47    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
48        if buf_len >= 8 && buf.starts_with(b"CatScene") {
49            return Some(255);
50        }
51        None
52    }
53}
54
55trait CustomFn {
56    fn write_patched_string(&mut self, s: &CstString, data: &[u8]) -> Result<usize>;
57}
58
59impl CustomFn for MemWriter {
60    fn write_patched_string(&mut self, s: &CstString, data: &[u8]) -> Result<usize> {
61        if data.len() + 1 > s.len {
62            let pos = self.data.len();
63            self.pos = pos;
64            self.write_u8(1)?; // Start marker
65            self.write_u8(u8::from(s.typ))?;
66            self.write_all(data)?;
67            self.write_u8(0)?; // Null terminator
68            Ok(pos)
69        } else {
70            self.pos = s.address;
71            self.write_u8(1)?; // Start marker
72            self.write_u8(u8::from(s.typ))?;
73            self.write_all(data)?;
74            self.write_u8(0)?; // Null terminator
75            Ok(s.address)
76        }
77    }
78}
79
80#[repr(u8)]
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, IntEnum)]
82enum CstStringType {
83    EmptyLine = 0x2,
84    Paragraph = 0x03,
85    Message = 0x20,
86    Character = 0x21,
87    Command = 0x30,
88    FileName = 0xF0,
89    LineNumber = 0xF1,
90}
91
92#[derive(Debug)]
93struct CstString {
94    typ: CstStringType,
95    text: String,
96    address: usize,
97    /// text length (include null terminator)
98    len: usize,
99}
100
101#[derive(Debug)]
102/// CatSystem2 Scene Script.
103pub struct CstScript {
104    data: MemReader,
105    compressed: bool,
106    strings: Vec<CstString>,
107    compress_level: u32,
108}
109
110impl CstScript {
111    /// Creates a new instance of `CstScript` from a buffer.
112    ///
113    /// * `buf` - The buffer containing the script data.
114    /// * `encoding` - The encoding of the script.
115    /// * `config` - Extra configuration options.
116    pub fn new(buf: Vec<u8>, encoding: Encoding, config: &ExtraConfig) -> Result<Self> {
117        let mut reader = MemReader::new(buf);
118        let mut magic = [0; 8];
119        reader.read_exact(&mut magic)?;
120        if &magic != b"CatScene" {
121            return Err(anyhow::anyhow!("Invalid CST script magic: {:?}", magic));
122        }
123        let compressed_size = reader.read_u32()?;
124        let uncompressed_size = reader.read_u32()?;
125        let mut file = if compressed_size == 0 {
126            if uncompressed_size != reader.data.len() as u32 - 0x10 {
127                return Err(anyhow::anyhow!(
128                    "Uncompressed size mismatch: expected {}, got {}",
129                    uncompressed_size,
130                    reader.data.len() as u32 - 0x10
131                ));
132            }
133            MemReader::new((&reader.data[0x10..]).to_vec())
134        } else {
135            let mut decoder = flate2::read::ZlibDecoder::new(reader);
136            let mut data = Vec::with_capacity(uncompressed_size as usize);
137            decoder.read_to_end(&mut data)?;
138            MemReader::new(data)
139        };
140        let data_length = file.read_u32()?;
141        if data_length as usize + 0x10 != file.data.len() {
142            return Err(anyhow::anyhow!(
143                "Data length mismatch: expected {}, got {}",
144                data_length,
145                file.data.len() - 0x10
146            ));
147        }
148        let _clear_screen_count = file.read_u32()?;
149        let string_address_offset = 0x10 + file.read_u32()?;
150        let strings_offset = 0x10 + file.read_u32()?;
151        let string_count = (strings_offset - string_address_offset) / 4;
152        let mut strings = Vec::with_capacity(string_count as usize);
153        for i in 0..string_count {
154            let offset = file.cpeek_u32_at(string_address_offset as u64 + i as u64 * 4)? as usize
155                + strings_offset as usize;
156            file.pos = offset;
157            let start_marker = file.read_u8()?;
158            if start_marker != 1 {
159                return Err(anyhow::anyhow!(
160                    "Invalid start marker for string {}: expected 0x01, got {:02X}",
161                    i,
162                    start_marker
163                ));
164            }
165            let typ = CstStringType::try_from(file.read_u8()?).map_err(|code| {
166                anyhow::anyhow!("Invalid string type for string {}: {:02X}", i, code)
167            })?;
168            let str = file.read_cstring()?;
169            let text = decode_to_string(encoding, str.as_bytes(), true)?;
170            strings.push(CstString {
171                typ,
172                text,
173                address: offset,
174                len: str.as_bytes_with_nul().len(),
175            });
176        }
177        Ok(CstScript {
178            data: file,
179            compressed: compressed_size != 0,
180            strings,
181            compress_level: config.zlib_compression_level,
182        })
183    }
184}
185
186lazy_static::lazy_static! {
187    static ref CST_COMMAND_REGEX: Regex = Regex::new(r"^\d+\s+\w+\s+(.+)").unwrap();
188}
189
190impl Script for CstScript {
191    fn default_output_script_type(&self) -> OutputScriptType {
192        OutputScriptType::Json
193    }
194
195    fn default_format_type(&self) -> FormatOptions {
196        FormatOptions::None
197    }
198
199    fn extract_messages(&self) -> Result<Vec<Message>> {
200        let mut messages = Vec::new();
201        let mut name = None;
202        for s in self.strings.iter() {
203            match s.typ {
204                CstStringType::Message => {
205                    if s.text.is_empty() {
206                        continue; // Skip empty messages
207                    }
208                    messages.push(Message {
209                        message: s.text.replace("\\n", "\n"),
210                        name: name.take(),
211                    });
212                }
213                CstStringType::Character => {
214                    name = Some(s.text.clone());
215                }
216                CstStringType::Command => {
217                    if let Some(caps) = CST_COMMAND_REGEX.captures(&s.text)? {
218                        if let Some(text) = caps.get(1) {
219                            messages.push(Message {
220                                message: text.as_str().to_string(),
221                                name: None,
222                            });
223                        }
224                    }
225                }
226                _ => {}
227            }
228        }
229        Ok(messages)
230    }
231
232    fn import_messages<'a>(
233        &'a self,
234        messages: Vec<Message>,
235        mut file: Box<dyn WriteSeek + 'a>,
236        _filename: &str,
237        encoding: Encoding,
238        replacement: Option<&'a ReplacementTable>,
239    ) -> Result<()> {
240        let mut writer = MemWriter::from_vec(self.data.data.clone());
241        let mut mess = messages.iter();
242        let mut mes = mess.next();
243        let strings_address_offset = 0x10 + self.data.cpeek_u32_at(0x8)? as usize;
244        let strings_offset = 0x10 + self.data.cpeek_u32_at(0xC)? as usize;
245        for (i, s) in self.strings.iter().enumerate() {
246            match s.typ {
247                CstStringType::Message => {
248                    if s.text.is_empty() {
249                        continue; // Skip empty messages
250                    }
251                    let m = match mes {
252                        Some(m) => m,
253                        None => {
254                            return Err(anyhow::anyhow!("No enough messages."));
255                        }
256                    };
257                    let mut message = m.message.clone();
258                    if let Some(replacement) = replacement {
259                        for (k, v) in &replacement.map {
260                            message = message.replace(k, v);
261                        }
262                    }
263                    message = message.replace("\n", "\\n");
264                    let data = encode_string(encoding, &message, true)?;
265                    let pos = writer.write_patched_string(s, &data)?;
266                    if pos != s.address {
267                        writer.write_u32_at(
268                            strings_address_offset as u64 + i as u64 * 4,
269                            (pos - strings_offset) as u32,
270                        )?;
271                    }
272                    mes = mess.next();
273                }
274                CstStringType::Character => {
275                    let m = match mes {
276                        Some(m) => m,
277                        None => {
278                            return Err(anyhow::anyhow!("No enough messages."));
279                        }
280                    };
281                    let mut name = match &m.name {
282                        Some(name) => name.to_owned(),
283                        None => return Err(anyhow::anyhow!("Message without name.")),
284                    };
285                    if let Some(replacement) = replacement {
286                        for (k, v) in &replacement.map {
287                            name = name.replace(k, v);
288                        }
289                    }
290                    let data = encode_string(encoding, &name, true)?;
291                    let pos = writer.write_patched_string(s, &data)?;
292                    if pos != s.address {
293                        writer.write_u32_at(
294                            strings_address_offset as u64 + i as u64 * 4,
295                            (pos - strings_offset) as u32,
296                        )?;
297                    }
298                }
299                CstStringType::Command => {
300                    if let Some(caps) = CST_COMMAND_REGEX.captures(&s.text)? {
301                        if let Some(mat) = caps.get(1) {
302                            let m = match mes {
303                                Some(m) => m,
304                                None => {
305                                    return Err(anyhow::anyhow!("No enough messages."));
306                                }
307                            };
308                            let mut text = m.message.clone();
309                            if let Some(replacement) = replacement {
310                                for (k, v) in &replacement.map {
311                                    text = text.replace(k, v);
312                                }
313                            }
314                            let mut command_text = s.text.clone();
315                            command_text.replace_range(mat.range(), &text);
316                            let data = encode_string(encoding, &command_text, true)?;
317                            let pos = writer.write_patched_string(s, &data)?;
318                            if pos != s.address {
319                                writer.write_u32_at(
320                                    strings_address_offset as u64 + i as u64 * 4,
321                                    (pos - strings_offset) as u32,
322                                )?;
323                            }
324                            mes = mess.next();
325                        }
326                    }
327                }
328                _ => {}
329            }
330        }
331        if mes.is_some() || mess.next().is_some() {
332            return Err(anyhow::anyhow!("Not all messages were processed."));
333        }
334        let data_len = writer.data.len() as u32 - 0x10;
335        writer.write_u32_at(0, data_len)?;
336        let data = writer.into_inner();
337        file.write_all(b"CatScene")?;
338        file.write_u32(0)?; // Compressed size
339        file.write_u32(data.len() as u32)?; // Uncompressed size
340        if self.compressed {
341            let mut encoder = flate2::write::ZlibEncoder::new(
342                &mut file,
343                flate2::Compression::new(self.compress_level),
344            );
345            encoder.write_all(&data)?;
346            encoder.finish()?;
347            let file_len = file.stream_position()?;
348            let compressed_size = (file_len as u32) - 0x10;
349            file.write_u32_at(8, compressed_size)?;
350        } else {
351            file.write_all(&data)?;
352        }
353        Ok(())
354    }
355}