Add export support for entis gls csx script (v1)

This commit is contained in:
2026-01-17 21:22:38 +08:00
parent ed4179473f
commit 3fa2cbec7a
6 changed files with 281 additions and 15 deletions

View File

@@ -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<Box<dyn Write + 'a>>,
addr: u32,
code: CSInstructionCode,
@@ -147,7 +147,7 @@ impl<'a> ECSExecutionImageDisassembler<'a> {
}
}
fn get_string_literal(&mut self) -> Result<String> {
pub fn get_string_literal(&mut self) -> Result<String> {
let (_, s) = self.get_string_literal2()?;
Ok(s)
}
@@ -163,7 +163,7 @@ impl<'a> ECSExecutionImageDisassembler<'a> {
})
}
fn read_csom(&mut self) -> Result<CSObjectMode> {
pub fn read_csom(&mut self) -> Result<CSObjectMode> {
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<CSVariableType> {
pub fn read_csvt(&mut self) -> Result<CSVariableType> {
let value = self.stream.read_u8()?;
CSVariableType::try_from(value).map_err(|_| {
anyhow::anyhow!(

View File

@@ -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<Vec<Message>> {
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<HashMap<String, Vec<Message>>> {
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<Vec<String>> {
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)
}
}

View File

@@ -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<u8>, _config: &ExtraConfig) -> Result<Self> {
pub fn new(buf: Vec<u8>, config: &ExtraConfig) -> Result<Self> {
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<Vec<Message>> {
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<std::collections::HashMap<String, Vec<Message>>> {
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(())
}
}