From 968abeec6dad7f6e3bb1a27d237b1b4bd0c47690 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Fri, 25 Jul 2025 09:24:01 +0800 Subject: [PATCH] Add new script hexen_haus support --- Cargo.lock | 5 +- Cargo.toml | 4 +- src/scripts/hexen_haus/bin.rs | 250 ++++++++++++++++++++++++++++++++++ src/scripts/hexen_haus/mod.rs | 1 + src/scripts/mod.rs | 4 + src/types.rs | 3 + 6 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 src/scripts/hexen_haus/bin.rs create mode 100644 src/scripts/hexen_haus/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f1bacd8..9826f44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -605,9 +605,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "miniz_oxide" @@ -635,6 +635,7 @@ dependencies = [ "json", "lazy_static", "libtlg-rs", + "memchr", "msg_tool_macro", "overf", "png", diff --git a/Cargo.toml b/Cargo.toml index e542faf..cd8ec0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ int-enum = { version = "1.2", optional = true } json = { version = "0.12", optional = true } lazy_static = "1.5.0" libtlg-rs = { version = "0.1", optional = true } +memchr = { version = "2.7", optional = true } msg_tool_macro = { path = "./msg_tool_macro" } overf = "0.1" png = { version = "0.17", optional = true } @@ -28,7 +29,7 @@ url = { version = "2.5", optional = true } utf16string = "0.2" [features] -default = ["artemis", "artemis-arc", "bgi", "bgi-arc", "bgi-img", "cat-system", "cat-system-arc", "cat-system-img", "circus", "escude", "escude-arc", "kirikiri", "kirikiri-img", "will-plus", "yaneurao", "yaneurao-itufuru"] +default = ["artemis", "artemis-arc", "bgi", "bgi-arc", "bgi-img", "cat-system", "cat-system-arc", "cat-system-img", "circus", "escude", "escude-arc", "hexen-haus", "kirikiri", "kirikiri-img", "will-plus", "yaneurao", "yaneurao-itufuru"] artemis = ["utils-escape"] artemis-arc = ["artemis", "msg_tool_macro/artemis-arc", "sha1"] bgi = [] @@ -40,6 +41,7 @@ cat-system-img = ["cat-system", "flate2", "image", "utils-bit-stream"] circus = [] escude = ["int-enum"] escude-arc = ["escude", "rand", "utils-bit-stream"] +hexen-haus = ["memchr", "utils-str"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "utils-escape"] kirikiri-img = ["kirikiri", "emote-psb", "image", "libtlg-rs", "url"] will-plus = ["utils-str"] diff --git a/src/scripts/hexen_haus/bin.rs b/src/scripts/hexen_haus/bin.rs new file mode 100644 index 0000000..227142f --- /dev/null +++ b/src/scripts/hexen_haus/bin.rs @@ -0,0 +1,250 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use crate::utils::str::*; +use anyhow::Result; +use std::io::Read; + +#[derive(Debug)] +pub struct BinScriptBuilder {} + +impl BinScriptBuilder { + pub fn new() -> Self { + BinScriptBuilder {} + } +} + +impl ScriptBuilder for BinScriptBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(BinScript::new(buf, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["bin"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::HexenHaus + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 && buf.starts_with(b"NORI") { + return Some(10); + } + None + } +} + +#[derive(Debug)] +struct BinString { + str: String, + pos: usize, + len: usize, +} + +#[derive(Debug)] +pub struct BinScript { + data: MemReader, + strs: Vec, +} + +impl BinScript { + pub fn new(buf: Vec, encoding: Encoding, _config: &ExtraConfig) -> Result { + let mut data = MemReader::new(buf); + let mut header = [0; 4]; + data.read_exact(&mut header)?; + if header != *b"NORI" { + return Err(anyhow::anyhow!("Invalid HexenHaus bin script header")); + } + for c in data.data.iter_mut() { + *c ^= 0x53; + } + data.pos = memchr::memmem::find(&data.data, b"_beginrp") + .ok_or(anyhow::anyhow!("Failed to find _beginrp"))?; + data.pos += 16; + let mut p = [0; 2]; + let mut s = Vec::new(); + let data_len = data.data.len(); + let mut start_pos = data.pos; + let mut strs = Vec::new(); + while data.pos < data_len { + data.read_exact(&mut p)?; + if p[0] == 0x53 { + if s.len() > 2 { + if let Ok(c) = decode_to_string(encoding, &s[s.len() - 2..], true) { + if c != "」" && c != "。" && c != "』" { + s.pop(); + s.pop(); + } + } else { + s.pop(); + s.pop(); + } + } + if s.len() > 2 { + let d = decode_to_string(encoding, &s, true)?; + strs.push(BinString { + str: d, + pos: start_pos, + len: s.len(), + }); + } + start_pos = data.pos; + s.clear(); + } else if p[1] == 0x53 { + if s.len() > 2 { + let d = decode_to_string(encoding, &s, true)?; + strs.push(BinString { + str: d, + pos: start_pos, + len: s.len(), + }); + } + start_pos = data.pos; + s.clear(); + } else { + s.extend_from_slice(&p); + } + } + if s.len() > 2 { + s.pop(); + s.pop(); + if s.len() > 2 { + let d = decode_to_string(encoding, &s, true)?; + strs.push(BinString { + str: d, + pos: start_pos, + len: s.len(), + }); + } + } + Ok(BinScript { data, strs }) + } +} + +impl Script for BinScript { + 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 = Vec::new(); + for str in &self.strs { + let message = if let Some(ind) = str.str.find("「") { + let (name, mes) = str.str.split_at(ind); + let mut name = name.to_string(); + if name.is_empty() { + if let Some(m) = messages.pop() { + name = m.message; + } + } + Message { + name: Some(name.to_string()), + message: mes.to_string(), + } + } else { + Message { + name: None, + message: str.str.clone(), + } + }; + messages.push(message); + } + Ok(messages) + } + + fn import_messages<'a>( + &'a self, + mut messages: Vec, + mut file: Box, + encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + let mut data = MemWriter::from_vec(self.data.data.clone()); + let mut i = 0; + for str in self.strs.iter() { + if i >= messages.len() { + return Err(anyhow::anyhow!("Not enough messages.")); + } + if let Some(ind) = str.str.find("「") { + let (name, _) = str.str.split_at(ind); + let mut target = String::new(); + if !name.is_empty() { + let mut name = match &messages[i].name { + Some(n) => n.to_owned(), + None => return Err(anyhow::anyhow!("Missing name for message.")), + }; + if let Some(repl) = replacement { + for (k, v) in &repl.map { + name = name.replace(k, v); + } + }; + target.push_str(&name); + } + let mut mes = messages[i].message.clone(); + if let Some(repl) = replacement { + for (k, v) in &repl.map { + mes = mes.replace(k, v); + } + } + target.push_str(&mes); + let mut encoded = encode_string(encoding, &target, false)?; + if encoded.len() > str.len { + eprintln!("Warning: Message '{}' is too long, truncating.", target); + crate::COUNTER.inc_warning(); + encoded = truncate_string(&target, str.len, encoding, false)?; + } + while encoded.len() < str.len { + encoded.push(32); // Fill with spaces + } + data.write_all_at(str.pos, &encoded)?; + i += 1; + } else { + let mut target = if let Some(name) = messages[i].name.take() { + name + } else { + let s = messages[i].message.clone(); + i += 1; + s + }; + if let Some(repl) = replacement { + for (k, v) in &repl.map { + target = target.replace(k, v); + } + } + let mut encoded = encode_string(encoding, &target, false)?; + if encoded.len() > str.len { + eprintln!("Warning: Message '{}' is too long, truncating.", target); + crate::COUNTER.inc_warning(); + encoded = truncate_string(&target, str.len, encoding, false)?; + } + while encoded.len() < str.len { + encoded.push(32); // Fill with spaces + } + data.write_all_at(str.pos, &encoded)?; + } + } + let mut data = data.into_inner(); + for d in data.iter_mut() { + *d ^= 0x53; + } + file.write_all(&data)?; + Ok(()) + } +} diff --git a/src/scripts/hexen_haus/mod.rs b/src/scripts/hexen_haus/mod.rs new file mode 100644 index 0000000..b6db8d7 --- /dev/null +++ b/src/scripts/hexen_haus/mod.rs @@ -0,0 +1 @@ +pub mod bin; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 73b7eef..733e5f4 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -9,6 +9,8 @@ pub mod cat_system; pub mod circus; #[cfg(feature = "escude")] pub mod escude; +#[cfg(feature = "hexen-haus")] +pub mod hexen_haus; #[cfg(feature = "kirikiri")] pub mod kirikiri; #[cfg(feature = "will-plus")] @@ -76,6 +78,8 @@ lazy_static::lazy_static! { Box::new(artemis::ast::AstScriptBuilder::new()), #[cfg(feature = "artemis")] Box::new(artemis::asb::ArtemisAsbBuilder::new()), + #[cfg(feature = "hexen-haus")] + Box::new(hexen_haus::bin::BinScriptBuilder::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 2c6e732..d5c5827 100644 --- a/src/types.rs +++ b/src/types.rs @@ -301,6 +301,9 @@ pub enum ScriptType { #[cfg(feature = "escude")] /// Escude list script EscudeList, + #[cfg(feature = "hexen-haus")] + /// HexenHaus bin script + HexenHaus, #[cfg(feature = "kirikiri")] #[value(alias("kr-scn"))] /// Kirikiri SCN script