diff --git a/.gitignore b/.gitignore index e280971..98f86f2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /testscripts /output /patched +*.log diff --git a/src/scripts/artemis/asb.rs b/src/scripts/artemis/asb.rs new file mode 100644 index 0000000..3d4058f --- /dev/null +++ b/src/scripts/artemis/asb.rs @@ -0,0 +1,300 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use crate::utils::escape::*; +use anyhow::Result; +use std::collections::BTreeMap; +use std::io::{Read, Write}; +use std::ops::Index; + +#[derive(Debug)] +pub struct ArtemisAsbBuilder {} + +impl ArtemisAsbBuilder { + pub fn new() -> Self { + ArtemisAsbBuilder {} + } +} + +impl ScriptBuilder for ArtemisAsbBuilder { + 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(Asb::new(buf, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["asb"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::ArtemisAsb + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 5 && buf.starts_with(b"ASB\0\0") { + return Some(20); + } + None + } +} + +#[derive(Clone, Debug)] +struct Command { + pub name: String, + pub line_number: u32, + pub attributes: BTreeMap, +} + +impl Command { + pub fn new(name: String, line_number: u32) -> Self { + Command { + name, + line_number, + attributes: BTreeMap::new(), + } + } + + pub fn to_xml(&self) -> String { + let mut xml = format!("<{}", self.name); + for (key, value) in &self.attributes { + xml.push_str(&format!(" {}=\"{}\"", key, escape_xml_text_value(value))); + } + xml.push('>'); + xml + } +} + +impl<'a> Index<&'a str> for Command { + type Output = str; + fn index(&self, key: &'a str) -> &Self::Output { + self.attributes.get(key).map_or("", |s| s.as_str()) + } +} + +impl<'a> Index<&'a String> for Command { + type Output = str; + fn index(&self, key: &'a String) -> &Self::Output { + self.attributes.get(key).map_or("", |s| s.as_str()) + } +} + +impl Index for Command { + type Output = str; + fn index(&self, key: String) -> &Self::Output { + self.attributes.get(&key).map_or("", |s| s.as_str()) + } +} + +#[derive(Clone, Debug)] +enum Item { + Command(Command), + Label(String), +} + +trait CustomReadFn { + fn read_string(&mut self, encoding: Encoding) -> Result; + fn read_item(&mut self, encoding: Encoding) -> Result; +} + +impl CustomReadFn for T { + fn read_string(&mut self, encoding: Encoding) -> Result { + let len = self.read_u32()?; + let data = self.read_exact_vec(len as usize)?; + if self.read_u8()? != 0 { + return Err(anyhow::anyhow!("String not null-terminated")); + } + let s = decode_to_string(encoding, &data, true)?; + Ok(s) + } + + fn read_item(&mut self, encoding: Encoding) -> Result { + let typ = self.read_u32()?; + match typ { + 0 => { + let name = self.read_string(encoding)?; + let line_number = self.read_u32()?; + let mut command = Command::new(name, line_number); + let attr_count = self.read_u32()?; + for _ in 0..attr_count { + let key = self.read_string(encoding)?; + let value = self.read_string(encoding)?; + command.attributes.insert(key, value); + } + Ok(Item::Command(command)) + } + 1 => { + let label = self.read_string(encoding)?; + Ok(Item::Label(label)) + } + _ => { + return Err(anyhow::anyhow!("Unknown item type: {}", typ)); + } + } + } +} + +trait CustomWriteFn { + fn write_string(&mut self, s: &str, encoding: Encoding) -> Result<()>; + fn write_item(&mut self, item: &Item, encoding: Encoding) -> Result<()>; +} + +impl CustomWriteFn for T { + fn write_string(&mut self, s: &str, encoding: Encoding) -> Result<()> { + let data = encode_string(encoding, s, false)?; + self.write_u32(data.len() as u32)?; + self.write_all(&data)?; + self.write_u8(0)?; // Null-terminated + Ok(()) + } + + fn write_item(&mut self, item: &Item, encoding: Encoding) -> Result<()> { + match item { + Item::Command(cmd) => { + self.write_u32(0)?; // Type 0 for Command + self.write_string(&cmd.name, encoding)?; + self.write_u32(cmd.line_number)?; + self.write_u32(cmd.attributes.len() as u32)?; + for (key, value) in &cmd.attributes { + self.write_string(key, encoding)?; + self.write_string(value, encoding)?; + } + } + Item::Label(label) => { + self.write_u32(1)?; // Type 1 for Label + self.write_string(label, encoding)?; + } + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct Asb { + items: Vec, +} + +impl Asb { + pub fn new(buf: Vec, encoding: Encoding, _config: &ExtraConfig) -> Result { + let mut data = MemReader::new(buf); + let mut magic = [0; 5]; + data.read_exact(&mut magic)?; + if &magic != b"ASB\0\0" { + return Err(anyhow::anyhow!("Invalid ASB magic number: {:?}", magic)); + } + let nums = data.read_u32()?; + let mut items = Vec::with_capacity(nums as usize); + for _ in 0..nums { + items.push(data.read_item(encoding)?); + } + Ok(Asb { items }) + } +} + +impl Script for Asb { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn extract_messages(&self) -> Result> { + let mut messages = Vec::new(); + let mut name = None; + let mut cur_mes = String::new(); + let mut in_print = false; + for item in self.items.iter() { + if in_print { + if let Item::Command(cmd) = item { + match cmd.name.as_str() { + "hcls" => { + in_print = false; + messages.push(Message { + name: name.take(), + message: cur_mes, + }); + cur_mes = String::new(); + } + "print" => { + cur_mes.push_str(&cmd["data"]); + } + "rt" => { + cur_mes.push('\n'); + } + _ => { + cur_mes.push_str(&cmd.to_xml()); + } + } + continue; + } + } + if let Item::Command(cmd) = item { + match cmd.name.as_str() { + "print" => { + cur_mes.push_str(&cmd["data"]); + in_print = true; + } + "name" => { + let v = (cmd.attributes.len() - 1).to_string(); + name = Some(cmd[v].to_owned()); + } + "sel_text" => { + let t = &cmd["text"]; + if !t.is_empty() { + messages.push(Message { + name: None, + message: t.to_owned(), + }); + } + } + "RegisterTextToHistory" => { + let t = &cmd["1"]; + if !t.is_empty() { + messages.push(Message { + name: None, + message: t.to_owned(), + }); + } + } + _ => {} + } + } + } + if !cur_mes.is_empty() { + messages.push(Message { + name: name.take(), + message: cur_mes, + }); + } + Ok(messages) + } + + fn import_messages<'a>( + &'a self, + _messages: Vec, + mut file: Box, + encoding: Encoding, + _replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + file.write_all(b"ASB\0\0")?; + let items = self.items.clone(); + file.write_u32(items.len() as u32)?; + for item in items { + file.write_item(&item, encoding)?; + } + file.flush()?; + Ok(()) + } +} diff --git a/src/scripts/artemis/mod.rs b/src/scripts/artemis/mod.rs index de3b312..c749590 100644 --- a/src/scripts/artemis/mod.rs +++ b/src/scripts/artemis/mod.rs @@ -1,3 +1,4 @@ #[cfg(feature = "artemis-arc")] pub mod archive; +pub mod asb; pub mod ast; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 7b1596b..73b7eef 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -74,6 +74,8 @@ lazy_static::lazy_static! { Box::new(artemis::archive::pfs::ArtemisArcBuilder::new()), #[cfg(feature = "artemis")] Box::new(artemis::ast::AstScriptBuilder::new()), + #[cfg(feature = "artemis")] + Box::new(artemis::asb::ArtemisAsbBuilder::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 843f444..2c6e732 100644 --- a/src/types.rs +++ b/src/types.rs @@ -241,6 +241,9 @@ pub enum ScriptType { #[cfg(feature = "artemis")] /// Artemis Engine AST script Artemis, + #[cfg(feature = "artemis")] + /// Artemis Engine ASB script + ArtemisAsb, #[cfg(feature = "artemis-arc")] #[value(alias("pfs"))] /// Artemis archive (pfs)