From 94b489f412ccb692327d6518ed5a530216e70d8a Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sun, 31 Aug 2025 00:06:02 +0800 Subject: [PATCH] Add new script Kirikiri TJS NS0 binary encoded script --- Cargo.lock | 2 - Cargo.toml | 2 +- README.md | 1 + msg_tool_macro/src/lib.rs | 10 +- src/scripts/ex_hibit/rld.rs | 6 +- src/scripts/kirikiri/mod.rs | 1 + src/scripts/kirikiri/tjs_ns0.rs | 174 ++++++++++++++++++++++++++++++++ src/scripts/mod.rs | 2 + src/types.rs | 16 +++ src/utils/encoding.rs | 14 +++ src/utils/struct_pack.rs | 6 +- 11 files changed, 220 insertions(+), 14 deletions(-) create mode 100644 src/scripts/kirikiri/tjs_ns0.rs diff --git a/Cargo.lock b/Cargo.lock index 08b6d34..e88b9f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1178,8 +1178,6 @@ dependencies = [ [[package]] name = "msg_tool_macro" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796c22de174c27a68ebb601d1a910187de399136406fb139fe2e2049742a5d93" dependencies = [ "quote", "syn 2.0.105", diff --git a/Cargo.toml b/Cargo.toml index d082330..ced97eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ markup5ever = { version = "0.35", optional = true } markup5ever_rcdom = { version = "0.35", optional = true } memchr = { version = "2.7", optional = true } mozjpeg = { version = "0.10", optional = true } -msg_tool_macro = { version = "0.2.0" } +msg_tool_macro = { path = "./msg_tool_macro" } overf = "0.1" pelite = { version = "0.10", optional = true } png = { version = "0.17", optional = true } diff --git a/README.md b/README.md index 24f7bad..3b0f41b 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ msg-tool create -t | `kirikiri-scn`/`kr-scn` | `kirikiri` | Kirikiri Scene File (.scn) | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | Patched script may be broken | | `kirikiri-simple-crypt`/`kr-simple-crypt` | `kirikiri` | Kirikiri Simple Crypt Text File | ❌ | ❌ | ✔️ | ❌ | ❌ | | | `kirikiri-mdf`/`kr-mdf` | `kirikiri` | Kirikiri Zlib-Compressed File | ❌ | ❌ | ✔️ | ❌ | ❌ | | +| `kirikiri-tjs-ns0`/`kr-tjs-ns0` | `kirikiri` | Kirikiri TJS NS0 binary encoded script | ❌ | ❌ | ✔️ | ❌ | ❌ | | | Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | |---|---|---|---|---|---|---|---|---| diff --git a/msg_tool_macro/src/lib.rs b/msg_tool_macro/src/lib.rs index e215b3b..1d38ebe 100644 --- a/msg_tool_macro/src/lib.rs +++ b/msg_tool_macro/src/lib.rs @@ -35,7 +35,7 @@ pub fn struct_unpack_impl_for_num(item: TokenStream) -> TokenStream { let i = syn::parse_macro_input!(item as syn::Ident); let output = quote::quote! { impl StructUnpack for #i { - fn unpack(mut reader: R, big: bool, _encoding: Encoding) -> Result { + fn unpack(reader: &mut R, big: bool, _encoding: Encoding) -> Result { let mut buf = [0u8; std::mem::size_of::<#i>()]; reader.read_exact(&mut buf)?; Ok(if big { @@ -587,7 +587,7 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { }); } else if let Some(pstring_type) = pstring_type { cur = Some(quote::quote! { - let len = <#pstring_type>::unpack(&mut reader, big, encoding)? as usize; + let len = <#pstring_type>::unpack(reader, big, encoding)? as usize; let #field_name = reader.read_exact_vec(len)?; let #field_name = crate::utils::encoding::decode_to_string(encoding, &#field_name, true)?; }); @@ -602,7 +602,7 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { }); } else if let Some(pvec_type) = pvec_type { cur = Some(quote::quote! { - let len = <#pvec_type>::unpack(&mut reader, big, encoding)? as usize; + let len = <#pvec_type>::unpack(reader, big, encoding)? as usize; let #field_name = reader.read_struct_vec(len, big, encoding)?; }); } @@ -611,7 +611,7 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { } let p = cur.unwrap_or_else(|| { quote::quote! { - let #field_name = <#field_type>::unpack(&mut reader, big, encoding)?; + let #field_name = <#field_type>::unpack(reader, big, encoding)?; } }); if let Some(skip_if) = skip_if { @@ -634,7 +634,7 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { }; let output = quote::quote! { impl StructUnpack for #name { - fn unpack(mut reader: R, big: bool, encoding: Encoding) -> Result { + fn unpack(reader: &mut R, big: bool, encoding: Encoding) -> Result { #(#smts)* Ok(Self #fields) } diff --git a/src/scripts/ex_hibit/rld.rs b/src/scripts/ex_hibit/rld.rs index b5e0eff..9784cc9 100644 --- a/src/scripts/ex_hibit/rld.rs +++ b/src/scripts/ex_hibit/rld.rs @@ -155,11 +155,11 @@ impl StructPack for OpExt { } impl StructUnpack for OpExt { - fn unpack(mut reader: R, big: bool, encoding: Encoding) -> Result { - let op = Op::unpack(&mut reader, big, encoding)?; + fn unpack(reader: &mut R, big: bool, encoding: Encoding) -> Result { + let op = Op::unpack(reader, big, encoding)?; let mut ints = Vec::with_capacity(op.init_count as usize); for _ in 0..op.init_count { - let i = u32::unpack(&mut reader, big, encoding)?; + let i = u32::unpack(reader, big, encoding)?; ints.push(i); } let mut strs = Vec::with_capacity(op.str_count() as usize); diff --git a/src/scripts/kirikiri/mod.rs b/src/scripts/kirikiri/mod.rs index 5f37844..074aa79 100644 --- a/src/scripts/kirikiri/mod.rs +++ b/src/scripts/kirikiri/mod.rs @@ -5,6 +5,7 @@ pub mod ks; pub mod mdf; pub mod scn; pub mod simple_crypt; +pub mod tjs_ns0; use std::collections::HashMap; use std::sync::Arc; diff --git a/src/scripts/kirikiri/tjs_ns0.rs b/src/scripts/kirikiri/tjs_ns0.rs new file mode 100644 index 0000000..c17d40c --- /dev/null +++ b/src/scripts/kirikiri/tjs_ns0.rs @@ -0,0 +1,174 @@ +//! Kirikiri TJS NS0 binary encoded script +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::{decode_to_string, encode_string}; +use crate::utils::struct_pack::*; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::io::{Read, Seek, Write}; + +#[derive(Debug)] +/// Kirikiri TJS NS0 Script Builder +pub struct TjsNs0Builder {} + +impl TjsNs0Builder { + /// Creates a new instance of `TjsNs0Builder` + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for TjsNs0Builder { + fn default_encoding(&self) -> Encoding { + Encoding::Utf16LE + } + + fn build_script( + &self, + buf: Vec, + filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(TjsNs0::new(buf, filename, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["tjs", "pbd"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::KirikiriTjsNs0 + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 12 && buf.starts_with(b"TJS/ns0\0TJS\0") { + return Some(100); + } + None + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +enum TjsValue { + Void(()), + Int(i64), + Str(String), + Array(Vec), + Dict(BTreeMap), +} + +fn unpack_string(reader: &mut R, big: bool, encoding: Encoding) -> Result { + let len = u32::unpack(reader, big, encoding)? as usize; + let tlen = if encoding.is_utf16le() { len * 2 } else { len }; + let mut buf = vec![0u8; tlen]; + reader.read_exact(&mut buf)?; + let s = decode_to_string(encoding, &buf, true)?; + Ok(s) +} + +impl StructUnpack for TjsValue { + fn unpack(reader: &mut R, big: bool, encoding: Encoding) -> Result { + let typ = u16::unpack(reader, big, encoding)?; + let typ_byte = (typ & 0xff) as u8; + Ok(match typ_byte { + 0 => TjsValue::Void(()), + 2 => TjsValue::Str(unpack_string(reader, big, encoding)?), + 4 => TjsValue::Int(i64::unpack(reader, big, encoding)?), + 0x81 => { + let arr_len = u32::unpack(reader, big, encoding)? as usize; + let mut arr = Vec::with_capacity(arr_len); + for _ in 0..arr_len { + arr.push(reader.read_struct::(big, encoding)?); + } + TjsValue::Array(arr) + } + 0xC1 => { + let kv_len = u32::unpack(reader, big, encoding)? as usize; + let mut dict = BTreeMap::new(); + for _ in 0..kv_len { + let key = unpack_string(reader, big, encoding)?; + let value = reader.read_struct::(big, encoding)?; + dict.insert(key, value); + } + TjsValue::Dict(dict) + } + _ => { + return Err(anyhow::anyhow!( + "Unsupported TJS/ns0 value type: {} at pos {}", + typ_byte, + reader.stream_position()? - 2 + )); + } + }) + } +} + +#[derive(Debug)] +/// Kirikiri TJS NS0 Script +pub struct TjsNs0 { + data: TjsValue, + custom_yaml: bool, +} + +impl TjsNs0 { + /// Creates a new `TjsNs0` script from the given buffer and filename + /// + /// * `buf` - The buffer containing the TJS/ns0 data + /// * `filename` - The name of the file + /// * `encoding` - The encoding to use for strings + /// * `config` - Extra configuration options + pub fn new( + buf: Vec, + _filename: &str, + encoding: Encoding, + config: &ExtraConfig, + ) -> Result { + let mut reader = MemReader::new(buf); + let mut header = [0u8; 16]; + reader.read_exact(&mut header)?; + if &header != b"TJS/ns0\0TJS\0\0\0\0\0" { + return Err(anyhow::anyhow!("Invalid TJS/ns0 header: {:?}", &header)); + } + let data = TjsValue::unpack(&mut reader, false, encoding)?; + Ok(Self { + data, + custom_yaml: config.custom_yaml, + }) + } +} + +impl Script for TjsNs0 { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Custom + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn is_output_supported(&self, output: OutputScriptType) -> bool { + matches!(output, OutputScriptType::Custom) + } + + fn custom_output_extension<'a>(&'a self) -> &'a str { + if self.custom_yaml { "yaml" } else { "json" } + } + + fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { + let s = if self.custom_yaml { + serde_yaml_ng::to_string(&self.data)? + } else { + serde_json::to_string_pretty(&self.data)? + }; + let s = encode_string(encoding, &s, false)?; + let mut writer = crate::utils::files::write_file(filename)?; + writer.write_all(&s)?; + Ok(()) + } +} diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 44eebab..733c499 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -114,6 +114,8 @@ lazy_static::lazy_static! { Box::new(softpal::scr::SoftpalScriptBuilder::new()), #[cfg(feature = "artemis-panmimisoft")] Box::new(artemis::panmimisoft::txt::TxtBuilder::new()), + #[cfg(feature = "kirikiri")] + Box::new(kirikiri::tjs_ns0::TjsNs0Builder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/types.rs b/src/types.rs index 319495c..10c1d6e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -15,6 +15,8 @@ pub enum Encoding { Cp932, /// GB2312 encoding Gb2312, + /// UTF-16 Little Endian encoding + Utf16LE, /// Code page encoding (Windows only) #[cfg(windows)] CodePage(u32), @@ -37,6 +39,16 @@ impl Encoding { } } + /// Returns true if the encoding is UTF-16LE. + pub fn is_utf16le(&self) -> bool { + match self { + Self::Utf16LE => true, + #[cfg(windows)] + Self::CodePage(code_page) => *code_page == 1200, + _ => false, + } + } + /// Returns true if the encoding is UTF8. pub fn is_utf8(&self) -> bool { match self { @@ -505,6 +517,10 @@ pub enum ScriptType { #[value(alias("kr-mdf"))] /// Kirikiri MDF (zlib compressed) file KirikiriMdf, + #[cfg(feature = "kirikiri")] + #[value(alias("kr-tjs-ns0"))] + /// Kirikiri TJS NS0 binary encoded script + KirikiriTjsNs0, #[cfg(feature = "softpal")] /// Softpal src script Softpal, diff --git a/src/utils/encoding.rs b/src/utils/encoding.rs index 67c189d..5c069ce 100644 --- a/src/utils/encoding.rs +++ b/src/utils/encoding.rs @@ -144,6 +144,16 @@ pub fn decode_to_string( } Ok(result) } + Encoding::Utf16LE => Ok(encoding::codec::utf_16::UTF_16LE_ENCODING + .decode( + data, + if check { + DecoderTrap::Strict + } else { + DecoderTrap::Replace + }, + ) + .map_err(|_| anyhow::anyhow!("Failed to decode UTF-16LE"))?), #[cfg(windows)] Encoding::CodePage(code_page) => Ok(super::encoding_win::decode_to_string( code_page, data, check, @@ -246,6 +256,10 @@ pub fn encode_string( }); Ok(result) } + Encoding::Utf16LE => { + let re = utf16string::WString::::from(data); + Ok(re.as_bytes().to_vec()) + } #[cfg(windows)] Encoding::CodePage(code_page) => { Ok(super::encoding_win::encode_string(code_page, data, check)?) diff --git a/src/utils/struct_pack.rs b/src/utils/struct_pack.rs index 680bfba..517857e 100644 --- a/src/utils/struct_pack.rs +++ b/src/utils/struct_pack.rs @@ -11,7 +11,7 @@ pub trait StructUnpack: Sized { /// * `reader` - The reader to read the binary data from. /// * `big` - Whether the data is in big-endian format. /// * `encoding` - The encoding to use for string fields. - fn unpack(reader: R, big: bool, encoding: Encoding) -> Result; + fn unpack(reader: &mut R, big: bool, encoding: Encoding) -> Result; } /// Trait for packing a struct into a binary stream. @@ -47,7 +47,7 @@ struct_unpack_impl_for_num!(f32); struct_unpack_impl_for_num!(f64); impl StructUnpack for bool { - fn unpack(mut reader: R, _big: bool, _encoding: Encoding) -> Result { + fn unpack(reader: &mut R, _big: bool, _encoding: Encoding) -> Result { let mut buf = [0u8; 1]; reader.read_exact(&mut buf)?; Ok(buf[0] != 0) @@ -71,7 +71,7 @@ impl StructPack for Option { } impl StructUnpack for Option { - fn unpack(reader: R, big: bool, encoding: Encoding) -> Result { + fn unpack(reader: &mut R, big: bool, encoding: Encoding) -> Result { let value = T::unpack(reader, big, encoding)?; Ok(Some(value)) }