diff --git a/src/args.rs b/src/args.rs index a8a66b0..11baa71 100644 --- a/src/args.rs +++ b/src/args.rs @@ -179,6 +179,11 @@ pub struct Arg { /// Specify the language of Artemis AST script. /// If not specified, the first language will be used. pub artemis_ast_lang: Option, + #[cfg(feature = "cat-system")] + #[arg(long, global = true)] + /// CatSystem2 CSTL script language, used to extract messages from CSTL script. + /// If not specified, the first language will be used. + pub cat_system_cstl_lang: Option, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index ab0d4b6..168033c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1447,6 +1447,8 @@ fn main() { artemis_max_line_width: arg.artemis_max_line_width, #[cfg(feature = "artemis")] artemis_ast_lang: arg.artemis_ast_lang.clone(), + #[cfg(feature = "cat-system")] + cat_system_cstl_lang: arg.cat_system_cstl_lang.clone(), }; match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/cat_system/cstl.rs b/src/scripts/cat_system/cstl.rs new file mode 100644 index 0000000..f692559 --- /dev/null +++ b/src/scripts/cat_system/cstl.rs @@ -0,0 +1,166 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use anyhow::Result; +use std::io::Read; + +#[derive(Debug)] +pub struct CstlScriptBuilder {} + +impl 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, + ) -> 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 + } +} + +#[derive(Debug)] +struct CstlScript { + langs: Vec, + data: Vec>, + lang_index: Option, +} + +impl CstlScript { + 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_u8()? as usize; + for _ in 0..lang_count { + let len = reader.read_u8()? as usize; + let s = reader.read_fstring(len, encoding, false)?; + langs.push(s); + data.push(Vec::new()); + } + let mut count = 0; + loop { + let len = reader.read_u8()?; + if len == 0 { + break; // End of data + } + count += len as usize; + } + let mut i = 0; + let mut name = None; + loop { + let len = reader.read_u8()?; + let s = reader.read_fstring(len as usize, encoding, false)?; + if reader.is_eof() { + data[i % lang_count].push(Message { + name: name.take(), + message: s, + }); + i += 1; + break; + } else { + let e = reader.read_u8()?; + if e != 0 { + data[i % lang_count].push(Message { + name: name.take(), + message: s, + }); + let s = reader.read_fstring(e as usize, encoding, false)?; + name = Some(s); + i += 1; + } else { + data[i % lang_count].push(Message { + name: name.take(), + message: s, + }); + i += 1; + } + } + } + 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, + }) + } +} + +impl Script for CstlScript { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + 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)].clone()) + } +} diff --git a/src/scripts/cat_system/mod.rs b/src/scripts/cat_system/mod.rs index 10c6a9d..6884b6c 100644 --- a/src/scripts/cat_system/mod.rs +++ b/src/scripts/cat_system/mod.rs @@ -1,5 +1,6 @@ #[cfg(feature = "cat-system-arc")] pub mod archive; pub mod cst; +pub mod cstl; #[cfg(feature = "cat-system-img")] pub mod image; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 1262f5a..6336743 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -82,6 +82,8 @@ lazy_static::lazy_static! { Box::new(hexen_haus::bin::BinScriptBuilder::new()), #[cfg(feature = "circus-img")] Box::new(circus::image::crx::CrxImageBuilder::new()), + #[cfg(feature = "cat-system")] + Box::new(cat_system::cstl::CstlScriptBuilder::new()), ]; pub static ref ALL_EXTS: Vec = BUILDER.iter().flat_map(|b| b.extensions()).map(|s| s.to_string()).collect(); diff --git a/src/types.rs b/src/types.rs index cf71811..8efe190 100644 --- a/src/types.rs +++ b/src/types.rs @@ -233,6 +233,8 @@ pub struct ExtraConfig { pub artemis_max_line_width: usize, #[cfg(feature = "artemis")] pub artemis_ast_lang: Option, + #[cfg(feature = "cat-system")] + pub cat_system_cstl_lang: Option, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] @@ -283,6 +285,9 @@ pub enum ScriptType { #[cfg(feature = "cat-system")] /// CatSystem2 engine scene script CatSystem, + #[cfg(feature = "cat-system")] + /// CatSystem2 engine CSTL script + CatSystemCstl, #[cfg(feature = "cat-system-arc")] /// CatSystem2 engine archive CatSystemInt, @@ -348,7 +353,7 @@ pub enum ScriptType { YaneuraoItufuruArc, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Message { #[serde(skip_serializing_if = "Option::is_none")] pub name: Option,