From 3fa2cbec7a34346e0f67915a023cb1e993c00816 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sat, 17 Jan 2026 21:22:38 +0800 Subject: [PATCH] Add export support for entis gls csx script (v1) --- src/args.rs | 4 + src/main.rs | 2 + src/scripts/entis_gls/csx/v1/disasm.rs | 10 +- src/scripts/entis_gls/csx/v1/img.rs | 220 +++++++++++++++++++++++++ src/scripts/entis_gls/csx/v1/mod.rs | 57 +++++-- src/types.rs | 3 + 6 files changed, 281 insertions(+), 15 deletions(-) diff --git a/src/args.rs b/src/args.rs index 82834e0..6ded788 100644 --- a/src/args.rs +++ b/src/args.rs @@ -613,6 +613,10 @@ pub struct Arg { /// Maximum amount of blocks to split into (0 for unlimited, but this can give extreme results that hurt compression on some files). /// Default value: 15. pub zopfli_maximum_block_splits: u16, + #[cfg(feature = "entis-gls")] + #[arg(long, global = true, action = ArgAction::SetTrue)] + /// Disassemble Entis GLS csx script when exporting in custom mode. + pub entis_gls_csx_diasm: bool, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index 72b7c0e..63e35f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3305,6 +3305,8 @@ fn main() { zopfli_maximum_block_splits: arg.zopfli_maximum_block_splits, #[cfg(feature = "artemis-panmimisoft")] artemis_panmimisoft_txt_multi_lang: arg.artemis_panmimisoft_txt_multi_lang, + #[cfg(feature = "entis-gls")] + entis_gls_csx_diasm: arg.entis_gls_csx_diasm, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/entis_gls/csx/v1/disasm.rs b/src/scripts/entis_gls/csx/v1/disasm.rs index ae0a37a..650b1c2 100644 --- a/src/scripts/entis_gls/csx/v1/disasm.rs +++ b/src/scripts/entis_gls/csx/v1/disasm.rs @@ -19,9 +19,9 @@ fn escape_string(s: &str) -> String { } pub struct ECSExecutionImageDisassembler<'a> { - stream: MemReaderRef<'a>, + pub stream: MemReaderRef<'a>, conststr: Option<&'a TaggedRefAddressList>, - assembly: ECSExecutionImageAssembly, + pub assembly: ECSExecutionImageAssembly, writer: Option>, addr: u32, code: CSInstructionCode, @@ -147,7 +147,7 @@ impl<'a> ECSExecutionImageDisassembler<'a> { } } - fn get_string_literal(&mut self) -> Result { + pub fn get_string_literal(&mut self) -> Result { let (_, s) = self.get_string_literal2()?; Ok(s) } @@ -163,7 +163,7 @@ impl<'a> ECSExecutionImageDisassembler<'a> { }) } - fn read_csom(&mut self) -> Result { + pub fn read_csom(&mut self) -> Result { let value = self.stream.read_u8()?; CSObjectMode::try_from(value).map_err(|_| { anyhow::anyhow!("Invalid CSObjectMode value: {} at {:08x}", value, self.addr) @@ -192,7 +192,7 @@ impl<'a> ECSExecutionImageDisassembler<'a> { }) } - fn read_csvt(&mut self) -> Result { + pub fn read_csvt(&mut self) -> Result { let value = self.stream.read_u8()?; CSVariableType::try_from(value).map_err(|_| { anyhow::anyhow!( diff --git a/src/scripts/entis_gls/csx/v1/img.rs b/src/scripts/entis_gls/csx/v1/img.rs index 0d73c35..3836956 100644 --- a/src/scripts/entis_gls/csx/v1/img.rs +++ b/src/scripts/entis_gls/csx/v1/img.rs @@ -4,8 +4,13 @@ use crate::ext::io::*; use crate::types::*; use crate::utils::struct_pack::*; use anyhow::Result; +use std::collections::HashMap; use std::io::Write; +use CSInstructionCode::*; +use CSObjectMode::*; +use CSVariableType::*; + #[derive(Clone, Debug)] #[allow(dead_code)] pub struct ECSExecutionImage { @@ -183,4 +188,219 @@ impl ECSExecutionImage { disasm.execute()?; Ok(()) } + + pub fn export(&self) -> Result> { + let mut disasm = ECSExecutionImageDisassembler::new( + self.image.to_ref(), + self.ext_const_str.as_ref(), + None, + ); + disasm.execute()?; + let mut messages = Vec::new(); + let assembly = disasm.assembly.clone(); + let mut name = None; + let mut pre_is_mess = false; + let mut string_stack = Vec::new(); + let mut message = String::new(); + for cmd in assembly.iter() { + if cmd.code == CsicLoad { + disasm.stream.pos = cmd.addr as usize + 1; + let csom = disasm.read_csom()?; + let csvt = disasm.read_csvt()?; + if csom == CsomImmediate && csvt == CsvtString { + let text = disasm.get_string_literal()?; + string_stack.insert(0, text); + if string_stack.len() > 8 { + string_stack.pop(); + } + } + } else if cmd.code == CsicCall { + disasm.stream.pos = cmd.addr as usize + 1; + let _csom = disasm.read_csom()?; + let num_args = disasm.stream.read_i32()?; + let func_name = WideString::unpack(&mut disasm.stream, false, Encoding::Utf16LE)?.0; + let mut is_mess = false; + if num_args == 1 { + if func_name == "SceneTitle" { + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "SceneTitle call without string argument at {:08X}", + cmd.addr + )); + } + messages.push(Message::new(string_stack[0].clone(), None)); + } else if func_name == "Mess" { + is_mess = true; + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "Mess call without string argument at {:08X}", + cmd.addr + )); + } + message.push_str(string_stack[0].as_str()); + } + } else if num_args == 2 { + if func_name == "Talk" { + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "Talk call without string argument at {:08X}", + cmd.addr + )); + } + name = Some(string_stack[0].clone()); + } else if func_name == "AddSelect" { + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "AddSelect call without string argument at {:08X}", + cmd.addr + )); + } + messages.push(Message::new(string_stack[0].clone(), None)); + } + } + if pre_is_mess && !is_mess { + messages.push(Message::new(message.clone(), name.take())); + message.clear(); + } + pre_is_mess = is_mess; + } + } + Ok(messages) + } + + pub fn export_multi(&self) -> Result>> { + let mut key = String::from("global"); + let mut messages = HashMap::new(); + let mut disasm = ECSExecutionImageDisassembler::new( + self.image.to_ref(), + self.ext_const_str.as_ref(), + None, + ); + disasm.execute()?; + let assembly = disasm.assembly.clone(); + let mut name = None; + let mut pre_is_mess = false; + let mut pre_is_enter = false; + let mut string_stack = Vec::new(); + let mut message = String::new(); + let mut pre_enter_name = String::new(); + for cmd in assembly.iter() { + let is_enter = cmd.code == CsicEnter; + if cmd.code == CsicLoad { + disasm.stream.pos = cmd.addr as usize + 1; + let csom = disasm.read_csom()?; + let csvt = disasm.read_csvt()?; + if csom == CsomImmediate && csvt == CsvtString { + let text = disasm.get_string_literal()?; + string_stack.insert(0, text); + if string_stack.len() > 8 { + string_stack.pop(); + } + } + } else if cmd.code == CsicCall { + disasm.stream.pos = cmd.addr as usize + 1; + let csom = disasm.read_csom()?; + let num_args = disasm.stream.read_i32()?; + let func_name = WideString::unpack(&mut disasm.stream, false, Encoding::Utf16LE)?.0; + let mut is_mess = false; + if num_args == 1 { + if func_name == "SceneTitle" { + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "SceneTitle call without string argument at {:08X}", + cmd.addr + )); + } + messages + .entry(key.clone()) + .or_insert_with(|| Vec::new()) + .push(Message::new(string_stack[0].clone(), None)); + } else if func_name == "Mess" { + is_mess = true; + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "Mess call without string argument at {:08X}", + cmd.addr + )); + } + if string_stack[0].starts_with("@") { + println!( + "Skipping message with special tag at {:08X}: {}", + cmd.addr, string_stack[0] + ); + continue; + } + message.push_str(string_stack[0].as_str()); + } + } else if num_args == 2 { + if func_name == "Talk" { + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "Talk call without string argument at {:08X}", + cmd.addr + )); + } + name = Some(string_stack[0].clone()); + } else if func_name == "AddSelect" { + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "AddSelect call without string argument at {:08X}", + cmd.addr + )); + } + messages + .entry(key.clone()) + .or_insert_with(|| Vec::new()) + .push(Message::new(string_stack[0].clone(), None)); + } + } else if num_args == 0 && csom == CsomAuto && func_name == "ScenarioEnter" { + if pre_is_enter { + key = pre_enter_name.clone(); + } else { + key = "global".to_string(); + } + } + if pre_is_mess && !is_mess { + messages + .entry(key.clone()) + .or_insert_with(|| Vec::new()) + .push(Message::new(message.clone(), name.take())); + message.clear(); + } + pre_is_mess = is_mess; + } else if is_enter { + disasm.stream.pos = cmd.addr as usize + 1; + let name = WideString::unpack(&mut disasm.stream, false, Encoding::Utf16LE)?.0; + let num_args = disasm.stream.read_i32()?; + if num_args == 0 { + pre_enter_name = name.clone(); + } + } + pre_is_enter = is_enter; + } + Ok(messages) + } + + pub fn export_all(&self) -> Result> { + let mut disasm = ECSExecutionImageDisassembler::new( + self.image.to_ref(), + self.ext_const_str.as_ref(), + None, + ); + disasm.execute()?; + let mut messages = Vec::new(); + let assembly = disasm.assembly.clone(); + for cmd in assembly.iter() { + if cmd.code == CsicLoad { + disasm.stream.pos = cmd.addr as usize + 1; + let csom = disasm.read_csom()?; + let csvt = disasm.read_csvt()?; + if csom == CsomImmediate && csvt == CsvtString { + let text = disasm.get_string_literal()?; + messages.push(text); + } + } + } + Ok(messages) + } } diff --git a/src/scripts/entis_gls/csx/v1/mod.rs b/src/scripts/entis_gls/csx/v1/mod.rs index cb30fc7..3c90535 100644 --- a/src/scripts/entis_gls/csx/v1/mod.rs +++ b/src/scripts/entis_gls/csx/v1/mod.rs @@ -7,6 +7,7 @@ mod types; use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; +use crate::utils::encoding::*; use anyhow::Result; use img::ECSExecutionImage; @@ -56,36 +57,72 @@ impl ScriptBuilder for CSXScriptBuilder { #[derive(Debug)] pub struct CSXScript { img: ECSExecutionImage, + disasm: bool, + custom_yaml: bool, } impl CSXScript { - pub fn new(buf: Vec, _config: &ExtraConfig) -> Result { + pub fn new(buf: Vec, config: &ExtraConfig) -> Result { let reader = MemReader::new(buf); let img = ECSExecutionImage::new(reader)?; - Ok(Self { img }) + Ok(Self { + img, + disasm: config.entis_gls_csx_diasm, + custom_yaml: config.custom_yaml, + }) } } impl Script for CSXScript { fn default_output_script_type(&self) -> OutputScriptType { - OutputScriptType::Custom + OutputScriptType::Json } - fn is_output_supported(&self, output: OutputScriptType) -> bool { - matches!(output, OutputScriptType::Custom) + fn is_output_supported(&self, _output: OutputScriptType) -> bool { + true } fn default_format_type(&self) -> FormatOptions { FormatOptions::None } - fn custom_output_extension<'a>(&'a self) -> &'a str { - "s" + fn extract_messages(&self) -> Result> { + self.img.export() } - fn custom_export(&self, filename: &std::path::Path, _encoding: Encoding) -> Result<()> { - let file = crate::utils::files::write_file(filename)?; - self.img.disasm(Box::new(file))?; + fn multiple_message_files(&self) -> bool { + true + } + + fn extract_multiple_messages(&self) -> Result>> { + self.img.export_multi() + } + + fn custom_output_extension<'a>(&'a self) -> &'a str { + if self.disasm { + "d.txt" + } else if self.custom_yaml { + "yaml" + } else { + "json" + } + } + + fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { + if self.disasm { + let file = crate::utils::files::write_file(filename)?; + self.img.disasm(Box::new(file))?; + } else { + let messages = self.img.export_all()?; + let s = if self.custom_yaml { + serde_yaml_ng::to_string(&messages)? + } else { + serde_json::to_string_pretty(&messages)? + }; + let s = encode_string(encoding, &s, false)?; + let mut file = crate::utils::files::write_file(filename)?; + file.write_all(&s)?; + } Ok(()) } } diff --git a/src/types.rs b/src/types.rs index 284d658..4bc9de9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -564,6 +564,9 @@ pub struct ExtraConfig { /// Maximum amount of blocks to split into (0 for unlimited, but this can give extreme results that hurt compression on some files). /// Default value: 15. pub zopfli_maximum_block_splits: u16, + #[cfg(feature = "entis-gls")] + /// Whether to disassemble Entis GLS csx script when exporting in custom mode. + pub entis_gls_csx_diasm: bool, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)]