msg_tool\scripts\cat_system/
cst.rs1use 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)]
12pub struct CstScriptBuilder {}
14
15impl CstScriptBuilder {
16 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)?; self.write_u8(u8::from(s.typ))?;
66 self.write_all(data)?;
67 self.write_u8(0)?; Ok(pos)
69 } else {
70 self.pos = s.address;
71 self.write_u8(1)?; self.write_u8(u8::from(s.typ))?;
73 self.write_all(data)?;
74 self.write_u8(0)?; 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 len: usize,
99}
100
101#[derive(Debug)]
102pub struct CstScript {
104 data: MemReader,
105 compressed: bool,
106 strings: Vec<CstString>,
107 compress_level: u32,
108}
109
110impl CstScript {
111 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; }
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; }
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)?; file.write_u32(data.len() as u32)?; 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}