diff --git a/src/ext/io.rs b/src/ext/io.rs index 0d65996..4152867 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -1,6 +1,7 @@ //!Extensions for IO operations. +use crate::types::Encoding; use crate::utils::encoding::decode_to_string; -use crate::{types::Encoding, utils::struct_pack::StructUnpack}; +use crate::utils::struct_pack::{StructPack, StructUnpack}; use std::ffi::CString; use std::io::*; use std::sync::Mutex; @@ -971,6 +972,13 @@ pub trait WriteExt { /// Writes a C-style string (null-terminated) to the writer. fn write_cstring(&mut self, value: &CString) -> Result<()>; + /// Write a struct to the writer. + fn write_struct( + &mut self, + value: &T, + big: bool, + encoding: Encoding, + ) -> Result<()>; } impl WriteExt for T { @@ -1032,6 +1040,17 @@ impl WriteExt for T { fn write_cstring(&mut self, value: &CString) -> Result<()> { self.write_all(value.as_bytes_with_nul()) } + + fn write_struct( + &mut self, + value: &V, + big: bool, + encoding: Encoding, + ) -> Result<()> { + value + .pack(self, big, encoding) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + } } /// A trait to help to write data to a writer at a specific offset. @@ -1144,6 +1163,9 @@ impl WriteAt for T { pub trait SeekExt { /// Returns the length of the stream. fn stream_length(&mut self) -> Result; + /// Aligns the current position to the given alignment. + /// Returns the new position after alignment. + fn align(&mut self, align: u64) -> Result; } impl SeekExt for T { @@ -1153,6 +1175,15 @@ impl SeekExt for T { self.seek(SeekFrom::Start(current_pos))?; Ok(length) } + + fn align(&mut self, align: u64) -> Result { + let current_pos = self.stream_position()?; + let aligned_pos = (current_pos + align - 1) & !(align - 1); + if aligned_pos != current_pos { + self.seek(SeekFrom::Start(aligned_pos))?; + } + Ok(aligned_pos) + } } /// A memory reader that can read data from a vector of bytes. @@ -1457,6 +1488,9 @@ impl Write for MemWriter { } impl Seek for MemWriter { + /// Seeks to a new position in the writer. + /// If the new position is beyond the current length of the data, the data is resized when writing. + /// (This means that seeking beyond the end does not immediately resize the data.) fn seek(&mut self, pos: SeekFrom) -> Result { match pos { SeekFrom::Start(offset) => { diff --git a/src/scripts/kirikiri/mod.rs b/src/scripts/kirikiri/mod.rs index 074aa79..bb1777e 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 tjs2; pub mod tjs_ns0; use std::collections::HashMap; use std::sync::Arc; diff --git a/src/scripts/kirikiri/tjs2.rs b/src/scripts/kirikiri/tjs2.rs new file mode 100644 index 0000000..5ce49f0 --- /dev/null +++ b/src/scripts/kirikiri/tjs2.rs @@ -0,0 +1,306 @@ +//! Kirikiri compiled TJS2 script +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::io::{Read, Seek, Write}; + +#[derive(Debug)] +/// Kirikiri TJS2 Script Builder +pub struct Tjs2Builder {} + +impl Tjs2Builder { + /// Creates a new instance of `Tjs2Builder` + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for Tjs2Builder { + 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(Tjs2::new(buf, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["tjs"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::KirikiriTjs2 + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + // TJS2 tag 100 version + if buf_len >= 8 && buf.starts_with(b"TJS2100\0") { + return Some(40); + } + None + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct DataArea { + byte_array: Vec, + short_array: Vec, + long_array: Vec, + longlong_array: Vec, + double_array: Vec, + string_array: Vec, + octet_array: Vec>, +} + +impl StructUnpack for DataArea { + fn unpack(reader: &mut R, big: bool, encoding: Encoding) -> Result { + reader.align(4)?; + let start_loc = reader.stream_position()?; + let mut data_tag = [0; 4]; + reader.read_exact(&mut data_tag)?; + if &data_tag != b"DATA" { + return Err(anyhow::anyhow!("Invalid DATA tag")); + } + let data_size = u32::unpack(reader, big, encoding)?; + let count = u32::unpack(reader, big, encoding)? as usize; + let byte_array = reader.read_exact_vec(count)?; + reader.align(4)?; + let short_count = u32::unpack(reader, big, encoding)? as usize; + let short_array = reader.read_struct_vec(short_count, big, encoding)?; + reader.align(4)?; + let long_count = u32::unpack(reader, big, encoding)? as usize; + let long_array = reader.read_struct_vec(long_count, big, encoding)?; + let longlong_count = u32::unpack(reader, big, encoding)? as usize; + let longlong_array = reader.read_struct_vec(longlong_count, big, encoding)?; + let double_count = u32::unpack(reader, big, encoding)? as usize; + let double_array = reader.read_struct_vec(double_count, big, encoding)?; + let str_count = u32::unpack(reader, big, encoding)? as usize; + let mut string_array = Vec::with_capacity(str_count); + for _ in 0..str_count { + let str_len = u32::unpack(reader, big, encoding)? as usize; + let str_bytes = reader.read_exact_vec(if encoding.is_utf16le() { + str_len * 2 + } else { + str_len + })?; + let s = decode_to_string(encoding, &str_bytes, true)?; + reader.align(4)?; + string_array.push(s); + } + let octet_count = u32::unpack(reader, big, encoding)? as usize; + let mut octet_array = Vec::with_capacity(octet_count); + for _ in 0..octet_count { + let octet_len = u32::unpack(reader, big, encoding)? as usize; + let octet_bytes = reader.read_exact_vec(octet_len)?; + reader.align(4)?; + octet_array.push(octet_bytes); + } + let end_loc = reader.stream_position()?; + if end_loc - start_loc != data_size as u64 { + return Err(anyhow::anyhow!( + "DATA size mismatch: expected {}, got {}", + data_size, + end_loc - start_loc + )); + } + Ok(DataArea { + byte_array, + short_array, + long_array, + longlong_array, + double_array, + string_array, + octet_array, + }) + } +} + +impl StructPack for DataArea { + fn pack(&self, writer: &mut W, big: bool, encoding: Encoding) -> Result<()> { + writer.write_all(b"DATA")?; + let mut tmp = MemWriter::new(); + tmp.write_struct(&(self.byte_array.len() as u32), big, encoding)?; + tmp.write_all(&self.byte_array)?; + tmp.align(4)?; + tmp.write_struct(&(self.short_array.len() as u32), big, encoding)?; + for v in &self.short_array { + tmp.write_struct(v, big, encoding)?; + } + tmp.align(4)?; + tmp.write_struct(&(self.long_array.len() as u32), big, encoding)?; + for v in &self.long_array { + tmp.write_struct(v, big, encoding)?; + } + tmp.write_struct(&(self.longlong_array.len() as u32), big, encoding)?; + for v in &self.longlong_array { + tmp.write_struct(v, big, encoding)?; + } + tmp.write_struct(&(self.double_array.len() as u32), big, encoding)?; + for v in &self.double_array { + tmp.write_struct(v, big, encoding)?; + } + tmp.write_struct(&(self.string_array.len() as u32), big, encoding)?; + for s in &self.string_array { + let encoded = encode_string(encoding, s, false)?; + let str_len = if encoding.is_utf16le() { + encoded.len() / 2 + } else { + encoded.len() + }; + tmp.write_struct(&(str_len as u32), big, encoding)?; + tmp.write_all(&encoded)?; + tmp.align(4)?; + } + tmp.write_struct(&(self.octet_array.len() as u32), big, encoding)?; + for o in &self.octet_array { + tmp.write_struct(&(o.len() as u32), big, encoding)?; + tmp.write_all(o)?; + tmp.align(4)?; + } + // make sure final size is aligned to 4 bytes + tmp.data.resize(tmp.pos, 0); + let data = tmp.into_inner(); + writer.write_struct(&(data.len() as u32 + 8), big, encoding)?; + writer.write_all(&data)?; + Ok(()) + } +} + +/// Kirikiri TJS2 Script +#[derive(Debug)] +pub struct Tjs2 { + data_area: DataArea, + remaing: Vec, + custom_yaml: bool, +} + +impl Tjs2 { + /// Creates a new `Tjs2` script from the given buffer + /// + /// * `buf` - The buffer containing the TJS2 data + /// * `encoding` - The encoding to use for strings + /// * `config` - Extra configuration options + pub fn new(buf: Vec, encoding: Encoding, config: &ExtraConfig) -> Result { + let mut reader = MemReader::new(buf); + let mut header = [0u8; 8]; + reader.read_exact(&mut header)?; + if &header != b"TJS2100\0" { + return Err(anyhow::anyhow!("Invalid TJS2 header: {:?}", &header)); + } + let _file_size = reader.read_u32()?; + let data_area = DataArea::unpack(&mut reader, false, encoding)?; + let mut remaing = Vec::new(); + reader.read_to_end(&mut remaing)?; + Ok(Self { + data_area, + remaing, + custom_yaml: config.custom_yaml, + }) + } +} + +impl Script for Tjs2 { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn is_output_supported(&self, _: OutputScriptType) -> bool { + true + } + + fn custom_output_extension<'a>(&'a self) -> &'a str { + if self.custom_yaml { "yaml" } else { "json" } + } + + fn extract_messages(&self) -> Result> { + let mut messages = Vec::new(); + for s in self.data_area.string_array.iter() { + messages.push(Message { + name: None, + message: s.clone(), + }); + } + Ok(messages) + } + + fn import_messages<'a>( + &'a self, + messages: Vec, + mut file: Box, + _filename: &str, + encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + let mut data_area = self.data_area.clone(); + data_area.string_array = messages + .iter() + .map(|m| { + let mut s = m.message.clone(); + if let Some(table) = replacement { + for (from, to) in &table.map { + s = s.replace(from, to); + } + } + s + }) + .collect(); + file.write_all(b"TJS2100\0")?; + file.write_u32(0)?; // placeholder for file size + data_area.pack(&mut file, false, encoding)?; + file.write_all(&self.remaing)?; + let file_size = file.stream_length()?; + file.write_u32_at(8, file_size as u32)?; // write actual file size + Ok(()) + } + + fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { + let s = if self.custom_yaml { + serde_yaml_ng::to_string(&self.data_area)? + } else { + serde_json::to_string_pretty(&self.data_area)? + }; + let encoded = encode_string(encoding, &s, false)?; + let mut file = crate::utils::files::write_file(filename)?; + file.write_all(&encoded)?; + Ok(()) + } + + fn custom_import<'a>( + &'a self, + custom_filename: &'a str, + mut file: Box, + encoding: Encoding, + output_encoding: Encoding, + ) -> Result<()> { + let data = crate::utils::files::read_file(custom_filename)?; + let s = decode_to_string(output_encoding, &data, true)?; + let data_area: DataArea = if self.custom_yaml { + serde_yaml_ng::from_str(&s)? + } else { + serde_json::from_str(&s)? + }; + file.write_all(b"TJS2100\0")?; + file.write_u32(0)?; // placeholder for file size + data_area.pack(&mut file, false, encoding)?; + file.write_all(&self.remaing)?; + let file_size = file.stream_length()?; + file.write_u32_at(8, file_size as u32)?; // write actual file size + Ok(()) + } +} diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 733c499..cae7cdf 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -116,6 +116,8 @@ lazy_static::lazy_static! { Box::new(artemis::panmimisoft::txt::TxtBuilder::new()), #[cfg(feature = "kirikiri")] Box::new(kirikiri::tjs_ns0::TjsNs0Builder::new()), + #[cfg(feature = "kirikiri")] + Box::new(kirikiri::tjs2::Tjs2Builder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/types.rs b/src/types.rs index 3c055ce..6811c53 100644 --- a/src/types.rs +++ b/src/types.rs @@ -518,6 +518,10 @@ pub enum ScriptType { /// Kirikiri MDF (zlib compressed) file KirikiriMdf, #[cfg(feature = "kirikiri")] + #[value(alias("kr-tjs2"))] + /// Kirikiri compiled TJS2 script + KirikiriTjs2, + #[cfg(feature = "kirikiri")] #[value(alias("kr-tjs-ns0"))] /// Kirikiri TJS NS0 binary encoded script KirikiriTjsNs0,