//! CatSystem2 Scene I18N File (.cstl) use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; use crate::utils::encoding::*; use anyhow::Result; use std::collections::BTreeMap; use std::io::{Read, Write}; #[derive(Debug)] /// Builder for CatSystem2 Scene I18N Script files. pub struct CstlScriptBuilder {} impl CstlScriptBuilder { /// Creates a new instance of `CstlScriptBuilder`. pub fn new() -> Self { CstlScriptBuilder {} } } impl ScriptBuilder for CstlScriptBuilder { fn default_encoding(&self) -> Encoding { Encoding::Utf8 } fn build_script( &self, buf: Vec, _filename: &str, encoding: Encoding, _archive_encoding: Encoding, config: &ExtraConfig, _archive: Option<&Box>, ) -> Result> { Ok(Box::new(CstlScript::new(buf, encoding, config)?)) } fn extensions(&self) -> &'static [&'static str] { &["cstl"] } fn script_type(&self) -> &'static ScriptType { &ScriptType::CatSystemCstl } fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { if buf_len >= 4 && buf.starts_with(b"CSTL") { return Some(15); } None } fn can_create_file(&self) -> bool { true } fn create_file<'a>( &'a self, filename: &'a str, writer: Box, encoding: Encoding, file_encoding: Encoding, config: &ExtraConfig, ) -> Result<()> { create_file( filename, writer, encoding, file_encoding, config.custom_yaml, ) } } /// Create a new CSTL file. /// /// * `custom_filename` - The path of input file. /// * `file` - The writer to write the CSTL file to. /// * `encoding` - The encoding of the CSTL file. /// * `output_encoding` - The encoding to use for the input file. /// * `yaml` - Whether to use YAML format. pub fn create_file( custom_filename: &str, mut file: T, encoding: Encoding, output_encoding: Encoding, yaml: bool, ) -> Result<()> { let input = crate::utils::files::read_file(custom_filename)?; let s = decode_to_string(output_encoding, &input, true)?; let data: BTreeMap> = if yaml { serde_yaml_ng::from_str(&s)? } else { serde_json::from_str(&s)? }; let count = data .first_key_value() .ok_or(anyhow::anyhow!("No data found in JSON"))? .1 .len(); for (lang, mess) in &data { if mess.len() != count { return Err(anyhow::anyhow!( "Language {lang} Message count mismatch: expected {}, got {}", count, mess.len() )); } } file.write_all(b"CSTL")?; file.write_u32(0)?; // unk let lang_count = data.len(); file.write_size(lang_count)?; for lang in data.keys() { let encoded = encode_string(encoding, lang, false)?; file.write_size(encoded.len())?; file.write_all(&encoded)?; } file.write_size(count)?; for i in 0..count { for mess in data.values() { let m = &mess[i]; if let Some(name) = &m.name { let encoded_name = encode_string(encoding, name, false)?; file.write_size(encoded_name.len())?; file.write_all(&encoded_name)?; } else { file.write_size(0)?; } let encoded_mes = encode_string(encoding, &m.message, false)?; file.write_size(encoded_mes.len())?; file.write_all(&encoded_mes)?; } } Ok(()) } trait CustomFn { fn read_size(&mut self) -> Result; } impl CustomFn for T { fn read_size(&mut self) -> Result { let mut size = 0; loop { let len = self.read_u8()?; size += len as usize; if len != 0xFF { break; } } Ok(size) } } trait CustomWriteFn { fn write_size(&mut self, size: usize) -> Result<()>; } impl CustomWriteFn for T { fn write_size(&mut self, mut size: usize) -> Result<()> { loop { let len = if size > 0xFF { 0xFF } else { size as u8 }; self.write_u8(len)?; size -= len as usize; if len != 0xFF { break; } } Ok(()) } } #[derive(Debug)] /// CSTL script. pub struct CstlScript { langs: Vec, data: Vec>, lang_index: Option, custom_yaml: bool, } impl CstlScript { /// Creates a new instance of `CstlScript` from a buffer. /// /// * `buf` - The buffer containing the script data. /// * `encoding` - The encoding of the script. /// * `config` - Extra configuration options. pub fn new(buf: Vec, encoding: Encoding, config: &ExtraConfig) -> Result { let mut langs = Vec::new(); let mut data = Vec::new(); let mut reader = MemReader::new(buf); let mut magic = [0; 4]; reader.read_exact(&mut magic)?; if &magic != b"CSTL" { return Err(anyhow::anyhow!("Invalid CSTL magic number")); } let unk = reader.read_u32()?; if unk != 0 { return Err(anyhow::anyhow!("Unknown CSTL unk value: {}", unk)); } let lang_count = reader.read_size()?; for _ in 0..lang_count { let len = reader.read_size()?; let s = reader.read_fstring(len, encoding, false)?; langs.push(s); data.push(Vec::new()); } let count = reader.read_size()?; let mut i = 0; loop { let name_len = reader.read_size()?; let name = if name_len > 0 { Some(reader.read_fstring(name_len, encoding, false)?) } else { None }; let mes_len = reader.read_size()?; let message = reader.read_fstring(mes_len, encoding, false)?; data[i % lang_count].push(Message { name, message }); i += 1; if reader.is_eof() { break; } } if i != count * lang_count { return Err(anyhow::anyhow!( "CSTL data count mismatch: expected {}, got {}", i, count * langs.len() )); } for (i, lang) in langs.iter().enumerate() { if data[i].len() != count { return Err(anyhow::anyhow!( "CSTL language '{}' data count mismatch: expected {}, got {}", lang, count, data[i].len() )); } } let lang_index = config .cat_system_cstl_lang .as_ref() .and_then(|lang| langs.iter().position(|l| l == lang)); if config.cat_system_cstl_lang.is_some() && lang_index.is_none() { eprintln!( "Warning: specified language '{}' not found in CSTL script", config.cat_system_cstl_lang.as_ref().unwrap() ); crate::COUNTER.inc_warning(); } Ok(CstlScript { langs, data, lang_index, custom_yaml: config.custom_yaml, }) } } impl Script for CstlScript { fn default_output_script_type(&self) -> OutputScriptType { OutputScriptType::Json } fn default_format_type(&self) -> FormatOptions { FormatOptions::None } fn is_output_supported(&self, _: OutputScriptType) -> bool { true } fn custom_output_extension<'a>(&'a self) -> &'a str { if self.custom_yaml { "yaml" } else { "json" } } fn extract_messages(&self) -> Result> { if self.langs.is_empty() || self.data.is_empty() { return Err(anyhow::anyhow!("CSTL script has no languages or data")); } Ok(self.data[self.lang_index.unwrap_or(0)] .iter() .map(|m| Message { name: m.name.clone(), message: m.message.replace("\\n", "\n"), }) .collect()) } fn import_messages<'a>( &'a self, messages: Vec, mut file: Box, _filename: &str, encoding: Encoding, replacement: Option<&'a ReplacementTable>, ) -> Result<()> { let mut data = self.data.clone(); let index = self.lang_index.unwrap_or(0); if data[index].len() != messages.len() { return Err(anyhow::anyhow!( "CSTL script language '{}' message count mismatch: expected {}, got {}", self.langs[index], data[index].len(), messages.len() )); } for (i, m) in data[index].iter_mut().enumerate() { if let Some(n) = &mut m.name { let mut name = match &messages[i].name { Some(name) => name.clone(), None => return Err(anyhow::anyhow!("Message {i} name is missing.")), }; if let Some(replacement) = replacement { for (k, v) in &replacement.map { name = name.replace(k, v); } } *n = name; } let mut mes = messages[i].message.clone(); if let Some(replacement) = replacement { for (k, v) in &replacement.map { mes = mes.replace(k, v); } } m.message = mes.replace("\n", "\\n"); } file.write_all(b"CSTL")?; file.write_u32(0)?; // unk let lang_count = self.langs.len(); file.write_size(lang_count)?; for lang in &self.langs { let encoded = encode_string(encoding, &lang, false)?; file.write_size(encoded.len())?; file.write_all(&encoded)?; } let count = data[index].len(); file.write_size(count)?; for i in 0..count { for j in 0..lang_count { let m = &data[j][i]; if let Some(name) = &m.name { let encoded_name = encode_string(encoding, name, false)?; file.write_size(encoded_name.len())?; file.write_all(&encoded_name)?; } else { file.write_size(0)?; } let encoded_mes = encode_string(encoding, &m.message, false)?; file.write_size(encoded_mes.len())?; file.write_all(&encoded_mes)?; } } Ok(()) } fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { let mut d = BTreeMap::new(); for (lang, data) in self.langs.iter().zip(&self.data) { d.insert(lang, data); } let s = if self.custom_yaml { serde_yaml_ng::to_string(&d) .map_err(|e| anyhow::anyhow!("Failed to serialize to YAML: {}", e))? } else { serde_json::to_string(&d) .map_err(|e| anyhow::anyhow!("Failed to serialize to JSON: {}", e))? }; let s = encode_string(encoding, &s, false)?; let mut file = std::fs::File::create(filename)?; file.write_all(&s)?; Ok(()) } fn custom_import<'a>( &'a self, custom_filename: &'a str, file: Box, encoding: Encoding, output_encoding: Encoding, ) -> Result<()> { create_file( custom_filename, file, encoding, output_encoding, self.custom_yaml, ) } }