From 8caa2a82b621b9833dc3253057d9ac53f0e745ff Mon Sep 17 00:00:00 2001 From: lifegpc Date: Wed, 21 Jan 2026 16:36:33 +0800 Subject: [PATCH] Add export support for csx v2 script --- src/args.rs | 14 ++ src/main.rs | 6 + src/scripts/entis_gls/csx/mod.rs | 32 ++- src/scripts/entis_gls/csx/v2/disasm.rs | 4 +- src/scripts/entis_gls/csx/v2/img.rs | 322 ++++++++++++++++++++++++- src/types.rs | 11 + 6 files changed, 382 insertions(+), 7 deletions(-) diff --git a/src/args.rs b/src/args.rs index f3cdfe1..d83d9ec 100644 --- a/src/args.rs +++ b/src/args.rs @@ -621,6 +621,20 @@ pub struct Arg { #[arg(long, global = true, default_value = "/")] /// The line feed character used in Entis GLS csx script. pub entis_gls_csx_lf: String, + #[cfg(feature = "entis-gls")] + #[arg(long, global = true)] + /// Entis GLS csx script version. + /// If not specified. Will try use lower version first. + pub entis_gls_csx_ver: Option, + #[cfg(feature = "entis-gls")] + #[arg(long, global = true)] + /// Entis GLS csx script version2 full version. + /// If not specified. Will try use higher version first. + pub entis_gls_csx_v2_ver: Option, + #[cfg(feature = "entis-gls")] + #[arg(long, global = true, action = ArgAction::SetTrue)] + /// Disable part labels in Entis GLS csx script when exporting. + pub entis_gls_csx_no_part_label: bool, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index feddd55..ab48699 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3309,6 +3309,12 @@ fn main() { entis_gls_csx_disasm: arg.entis_gls_csx_disasm, #[cfg(feature = "entis-gls")] entis_gls_csx_lf: arg.entis_gls_csx_lf.clone(), + #[cfg(feature = "entis-gls")] + entis_gls_csx_ver: arg.entis_gls_csx_ver, + #[cfg(feature = "entis-gls")] + entis_gls_csx_v2_ver: arg.entis_gls_csx_v2_ver, + #[cfg(feature = "entis-gls")] + entis_gls_csx_no_part_label: arg.entis_gls_csx_no_part_label, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/entis_gls/csx/mod.rs b/src/scripts/entis_gls/csx/mod.rs index 03906f2..5694a4e 100644 --- a/src/scripts/entis_gls/csx/mod.rs +++ b/src/scripts/entis_gls/csx/mod.rs @@ -15,6 +15,25 @@ use base::ECSImage; use v1::ECSExecutionImageV1; use v2::ECSExecutionImageV2; +#[derive(Clone, Copy, Debug, clap::ValueEnum, PartialEq, Eq, PartialOrd, Ord)] +/// CSX Script Version +pub enum CSXScriptVersion { + /// Version 1 + V1, + /// Version 2 (2.x-3.x) + V2, +} + +#[derive(Clone, Copy, Debug, clap::ValueEnum, PartialEq, Eq, PartialOrd, Ord)] +/// CSX Script Full Version +pub enum CSXScriptV2FullVer { + /// Version 2.x + V2, + /// Version 3.x + /// Example game: お兄ちゃん、右手の使用を禁止します! + V3, +} + #[derive(Debug)] pub struct CSXScriptBuilder {} @@ -68,7 +87,18 @@ pub struct CSXScript { impl CSXScript { pub fn new(buf: Vec, config: &ExtraConfig) -> Result { let reader = MemReader::new(buf); - let img = { + let img = if let Some(ver) = &config.entis_gls_csx_ver { + match ver { + CSXScriptVersion::V1 => { + Box::new(ECSExecutionImageV1::new(reader.to_ref(), config)?) + as Box + } + CSXScriptVersion::V2 => { + Box::new(ECSExecutionImageV2::new(reader.to_ref(), config)?) + as Box + } + } + } else { match ECSExecutionImageV1::new(reader.to_ref(), config) { Ok(img) => Box::new(img), Err(_) => Box::new(ECSExecutionImageV2::new(reader.to_ref(), config)?) diff --git a/src/scripts/entis_gls/csx/v2/disasm.rs b/src/scripts/entis_gls/csx/v2/disasm.rs index d6c7bf9..af58ee8 100644 --- a/src/scripts/entis_gls/csx/v2/disasm.rs +++ b/src/scripts/entis_gls/csx/v2/disasm.rs @@ -25,7 +25,7 @@ pub struct ECSExecutionImageDisassembler<'a> { writer: Option>, addr: u32, code: CSInstructionCode, - func_map: HashMap, + pub func_map: HashMap, } impl<'a> ECSExecutionImageDisassembler<'a> { @@ -286,7 +286,7 @@ impl<'a> ECSExecutionImageDisassembler<'a> { }) } - fn read_csot(&mut self) -> Result { + pub fn read_csot(&mut self) -> Result { let value = self.stream.read_u8()?; CSOperatorType::try_from(value).map_err(|_| { anyhow::anyhow!( diff --git a/src/scripts/entis_gls/csx/v2/img.rs b/src/scripts/entis_gls/csx/v2/img.rs index c94748a..47b5af1 100644 --- a/src/scripts/entis_gls/csx/v2/img.rs +++ b/src/scripts/entis_gls/csx/v2/img.rs @@ -1,3 +1,4 @@ +use super::super::CSXScriptV2FullVer; use super::super::base::*; use super::disasm::*; use super::types::*; @@ -52,10 +53,25 @@ pub struct ECSExecutionImage { section_ref_code: Option, section_ref_class: Option, section_import_native_func: SectionImportNativeFunc, + no_part_label: bool, } impl ECSExecutionImage { - pub fn new(mut reader: MemReaderRef<'_>, _config: &ExtraConfig) -> Result { + pub fn new(reader: MemReaderRef<'_>, config: &ExtraConfig) -> Result { + if let Some(ver) = config.entis_gls_csx_v2_ver { + match ver { + CSXScriptV2FullVer::V3 => Self::inner_new(reader, config, 3), + CSXScriptV2FullVer::V2 => Self::inner_new(reader, config, 2), + } + } else { + match Self::inner_new(reader.clone(), config, 3) { + Ok(img) => Ok(img), + Err(_) => Self::inner_new(reader, config, 2), + } + } + } + + fn inner_new(mut reader: MemReaderRef<'_>, config: &ExtraConfig, ver: u32) -> Result { let file_header = FileHeader::unpack(&mut reader, false, Encoding::Utf8, &None)?; if file_header.signagure != *b"Entis\x1a\0\0" { return Err(anyhow::anyhow!("Invalid EMC file signature")); @@ -64,6 +80,7 @@ impl ECSExecutionImage { return Err(anyhow::anyhow!("Invalid EMC file format description")); } let mut section_header = SectionHeader::default(); + section_header.full_ver = ver; let len = reader.data.len(); let mut image = None; let mut image_global = None; @@ -97,6 +114,7 @@ impl ECSExecutionImage { ID_HEADER => { let mut mem = StreamRegion::with_size(&mut reader, size)?; section_header = SectionHeader::unpack(&mut mem, false, Encoding::Utf8, &None)?; + section_header.full_ver = ver; } ID_IMAGE => { image = Some(MemReader::new(reader.read_exact_vec(size as usize)?)); @@ -355,6 +373,7 @@ impl ECSExecutionImage { section_ref_class, section_import_native_func: section_import_native_func .ok_or_else(|| anyhow::anyhow!("Missing import native func section"))?, + no_part_label: config.entis_gls_csx_no_part_label, }) } } @@ -375,15 +394,310 @@ impl ECSImage for ECSExecutionImage { } fn export(&self) -> Result> { - Err(anyhow::anyhow!("Export not implemented for CSX v2")) + let mut messages = Vec::new(); + let mut disasm = ECSExecutionImageDisassembler::new( + self.image.to_ref(), + &self.section_function, + &self.section_func_info, + &self.section_import_native_func, + &self.section_class_info, + &self.section_const_string, + None, + ); + disasm.execute()?; + let assembly = disasm.assembly.clone(); + let mut string_stack = Vec::new(); + let mut stacks = Vec::new(); + let mut index = 0; + let len = assembly.len(); + while index < len { + let cmd = &assembly[index]; + if cmd.code == CsicLoad { + disasm.stream.pos = cmd.addr as usize + 1; + let csom = disasm.read_csom()?; + let csvt = disasm.read_csvt()?; + let is_string = csom == CsomImmediate && csvt == CsvtString; + if is_string { + let s = disasm.get_string_literal()?; + string_stack.push(s); + } + stacks.push(is_string); + } else if matches!( + cmd.code, + CsicCall + | CsicCallMember + | CsicCallNativeFunction + | CsicCallNativeMember + | CsicEnter + | CsicElementIndirect + ) { + string_stack.clear(); + stacks.clear(); + } else if cmd.code == CsicOperate { + disasm.stream.pos = cmd.addr as usize + 1; + let csot = disasm.read_csot()?; + if csot == CsotAdd { + if string_stack.len() >= 2 + && index >= 2 + && stacks.len() >= 2 + && stacks[stacks.len() - 1] + && stacks[stacks.len() - 2] + { + let s2 = string_stack.pop().unwrap(); + let s1 = string_stack.pop().unwrap(); + let s = s1 + &s2; + string_stack.push(s); + stacks.pop(); + // Remove the two previous load commands and replace with this one + index += 1; + continue; + } + } + if let Some(is_str) = stacks.pop() { + if is_str && string_stack.is_empty() { + return Err(anyhow::anyhow!( + "String stack is empty when processing Operate at {:08X}", + cmd.addr, + )); + } + if is_str { + string_stack.pop(); + } + } + } else if cmd.code == CsicExCall { + disasm.stream.pos = cmd.addr as usize + 1; + let arg_count = disasm.stream.read_i32()?; + let csom = disasm.read_csom()?; + let csvt = disasm.read_csvt()?; + if csom == CsomImmediate { + let func_name = if csvt == CsvtString { + disasm.get_string_literal()? + } else if csvt == CsvtInteger { + let func_address = disasm.stream.read_u32()?; + let func = disasm.func_map.get(&func_address).ok_or_else(|| { + anyhow::anyhow!( + "Function address 0x{:08X} not found in ExCall", + func_address + ) + })?; + func.name.0.clone() + } else { + return Err(anyhow::anyhow!( + "Unexpected CSVT for function name in ExCall" + )); + }; + if func_name == "WitchWizard::OutMsg" && arg_count == 8 { + if string_stack.len() < 2 { + return Err(anyhow::anyhow!( + "String stack has less than 2 items when processing OutMsg at {:08X}", + cmd.addr, + )); + } + if string_stack.len() > 2 { + eprintln!( + "WARNING: String stack has more than 2 items when processing OutMsg at {:08X}", + cmd.addr, + ); + crate::COUNTER.inc_warning(); + } + let name = string_stack[0].clone(); + let message = string_stack[1].clone(); + messages.push(Message { + name: if name.is_empty() { None } else { Some(name) }, + message, + }); + } + } + string_stack.clear(); + stacks.clear(); + } + index += 1; + } + Ok(messages) } fn export_multi(&self) -> Result>> { - Err(anyhow::anyhow!("Export multi not implemented for CSX v2")) + let mut key = String::from("global"); + let mut messages: HashMap> = HashMap::new(); + let mut disasm = ECSExecutionImageDisassembler::new( + self.image.to_ref(), + &self.section_function, + &self.section_func_info, + &self.section_import_native_func, + &self.section_class_info, + &self.section_const_string, + None, + ); + disasm.execute()?; + let assembly = disasm.assembly.clone(); + let mut string_stack = Vec::new(); + let mut stacks = Vec::new(); + let mut index = 0; + let len = assembly.len(); + while index < len { + let cmd = &assembly[index]; + if cmd.code == CsicLoad { + disasm.stream.pos = cmd.addr as usize + 1; + let csom = disasm.read_csom()?; + let csvt = disasm.read_csvt()?; + let is_string = csom == CsomImmediate && csvt == CsvtString; + if is_string { + let s = disasm.get_string_literal()?; + string_stack.push(s); + } + stacks.push(is_string); + } else if matches!( + cmd.code, + CsicCall + | CsicCallMember + | CsicCallNativeFunction + | CsicCallNativeMember + | CsicEnter + | CsicElementIndirect + ) { + string_stack.clear(); + stacks.clear(); + } else if cmd.code == CsicOperate { + disasm.stream.pos = cmd.addr as usize + 1; + let csot = disasm.read_csot()?; + if csot == CsotAdd { + if string_stack.len() >= 2 + && index >= 2 + && stacks.len() >= 2 + && stacks[stacks.len() - 1] + && stacks[stacks.len() - 2] + { + let s2 = string_stack.pop().unwrap(); + let s1 = string_stack.pop().unwrap(); + let s = s1 + &s2; + string_stack.push(s); + stacks.pop(); + // Remove the two previous load commands and replace with this one + index += 1; + continue; + } + } + if let Some(is_str) = stacks.pop() { + if is_str && string_stack.is_empty() { + return Err(anyhow::anyhow!( + "String stack is empty when processing Operate at {:08X}", + cmd.addr, + )); + } + if is_str { + string_stack.pop(); + } + } + } else if cmd.code == CsicExCall { + disasm.stream.pos = cmd.addr as usize + 1; + let arg_count = disasm.stream.read_i32()?; + let csom = disasm.read_csom()?; + let csvt = disasm.read_csvt()?; + if csom == CsomImmediate { + let func_name = if csvt == CsvtString { + disasm.get_string_literal()? + } else if csvt == CsvtInteger { + let func_address = disasm.stream.read_u32()?; + let func = disasm.func_map.get(&func_address).ok_or_else(|| { + anyhow::anyhow!( + "Function address 0x{:08X} not found in ExCall", + func_address + ) + })?; + func.name.0.clone() + } else { + return Err(anyhow::anyhow!( + "Unexpected CSVT for function name in ExCall" + )); + }; + if func_name == "WitchWizard::SetPastLabel" + && arg_count == 2 + && !self.no_part_label + { + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "String stack is empty when processing SetPastLabel" + )); + } + if string_stack.len() > 1 { + eprintln!( + "WARNING: String stack has more than 1 item when processing SetPastLabel at {:08X}", + cmd.addr, + ); + crate::COUNTER.inc_warning(); + } + key = string_stack[0].clone(); + } else if func_name == "WitchWizard::OutMsg" && arg_count == 8 { + if string_stack.len() < 2 { + return Err(anyhow::anyhow!( + "String stack has less than 2 items when processing OutMsg at {:08X}", + cmd.addr, + )); + } + if string_stack.len() > 2 { + eprintln!( + "WARNING: String stack has more than 2 items when processing OutMsg at {:08X}", + cmd.addr, + ); + crate::COUNTER.inc_warning(); + } + let name = string_stack[0].clone(); + let message = string_stack[1].clone(); + messages + .entry(key.clone()) + .or_insert_with(Vec::new) + .push(Message { + name: if name.is_empty() { None } else { Some(name) }, + message, + }); + } else if func_name == "WitchWizard::SetCurrentScriptName" && arg_count == 2 { + if string_stack.is_empty() { + return Err(anyhow::anyhow!( + "String stack is empty when processing SetCurrentScriptName" + )); + } + if string_stack.len() > 1 { + eprintln!( + "WARNING: String stack has more than 1 item when processing SetCurrentScriptName at {:08X}", + cmd.addr, + ); + crate::COUNTER.inc_warning(); + } + key = string_stack[0].clone(); + } + } + string_stack.clear(); + stacks.clear(); + } + index += 1; + } + Ok(messages) } fn export_all(&self) -> Result> { - Err(anyhow::anyhow!("Export all not implemented for CSX v2")) + let mut disasm = ECSExecutionImageDisassembler::new( + self.image.to_ref(), + &self.section_function, + &self.section_func_info, + &self.section_import_native_func, + &self.section_class_info, + &self.section_const_string, + None, + ); + disasm.execute()?; + let mut messages = Vec::new(); + for cmd in disasm.assembly.clone().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 s = disasm.get_string_literal()?; + messages.push(s); + } + } + } + Ok(messages) } fn import<'a>( diff --git a/src/types.rs b/src/types.rs index e3983d5..d6fa202 100644 --- a/src/types.rs +++ b/src/types.rs @@ -571,6 +571,17 @@ pub struct ExtraConfig { #[default(String::from("/"))] /// The line feed character used in Entis GLS csx script. pub entis_gls_csx_lf: String, + #[cfg(feature = "entis-gls")] + /// Entis GLS csx script version. + /// If not specified. Will try use lower version first. + pub entis_gls_csx_ver: Option, + #[cfg(feature = "entis-gls")] + /// Entis GLS csx script version2 full version. + /// If not specified. Will try use higher version first. + pub entis_gls_csx_v2_ver: Option, + #[cfg(feature = "entis-gls")] + /// Disable part labels in Entis GLS csx script when exporting. + pub entis_gls_csx_no_part_label: bool, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)]