From 900678f89a429d1cdf8d53dc7175970dd102cc85 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Fri, 5 Sep 2025 20:35:54 +0800 Subject: [PATCH] Add silky engine mes support --- Cargo.toml | 3 +- README.md | 4 + src/ext/io.rs | 102 ++++++++ src/scripts/mod.rs | 4 + src/scripts/silky/disasm.rs | 457 ++++++++++++++++++++++++++++++++++++ src/scripts/silky/mes.rs | 426 +++++++++++++++++++++++++++++++++ src/scripts/silky/mod.rs | 2 + src/types.rs | 3 + 8 files changed, 1000 insertions(+), 1 deletion(-) create mode 100644 src/scripts/silky/disasm.rs create mode 100644 src/scripts/silky/mes.rs create mode 100644 src/scripts/silky/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 741e775..b186b43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ zstd = { version = "0.13", optional = true } [features] default = ["all-fmt", "image-jpg", "image-webp", "audio-flac"] all-fmt = ["all-script", "all-img", "all-arc", "all-audio"] -all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "hexen-haus", "kirikiri", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"] +all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "hexen-haus", "kirikiri", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"] all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "kirikiri-img"] all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc"] all-audio = ["bgi-audio", "circus-audio"] @@ -74,6 +74,7 @@ ex-hibit = [] hexen-haus = ["memchr", "utils-str"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "utils-escape"] kirikiri-img = ["kirikiri", "image", "libtlg-rs"] +silky = [] softpal = ["int-enum"] will-plus = ["utils-str"] yaneurao = [] diff --git a/README.md b/README.md index a93282d..b28b324 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,10 @@ msg-tool create -t | Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | |---|---|---|---|---|---|---|---|---| | `kirikiri-tlg`/`kr-tlg` | `kirikiri-img` | Kirikiri TLG Image File (.tlg) | ✔️ | ✔️ | ❌ | ❌ | ✔️ | tlg6 is not supported when importing/creating image | +### Silky Engine +| Script Type | Feature Name | Name | Export | Import | Custom Export | Custom Import | Create | Remarks | +|---|---|---|---|---|---|---|---|---| +| `silky` | `silky` | Silky Engine Mes Script File (.mes) | ✔️ | ✔️ | ❌ | ❌ | ❌ | | ### Softpal | Script Type | Feature Name | Name | Export | Import | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---| diff --git a/src/ext/io.rs b/src/ext/io.rs index 4152867..a4bcf8d 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -259,6 +259,40 @@ pub trait Peek { fn peek_cstring(&mut self) -> Result; /// Peeks a C-style string (null-terminated) from the reader at a specific offset. fn peek_cstring_at(&mut self, offset: u64) -> Result; + /// Peeks a fixed-length string from the reader. + fn peek_fstring(&mut self, len: usize, encoding: Encoding, trim: bool) -> Result { + let mut buf = vec![0u8; len]; + self.peek_exact(&mut buf)?; + if trim { + let first_zero = buf.iter().position(|&b| b == 0); + if let Some(pos) = first_zero { + buf.truncate(pos); + } + } + let s = decode_to_string(encoding, &buf, true) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok(s) + } + /// Peeks a fixed-length string from the reader at a specific offset. + fn peek_fstring_at( + &mut self, + offset: u64, + len: usize, + encoding: Encoding, + trim: bool, + ) -> Result { + let mut buf = vec![0u8; len]; + self.peek_exact_at(offset, &mut buf)?; + if trim { + let first_zero = buf.iter().position(|&b| b == 0); + if let Some(pos) = first_zero { + buf.truncate(pos); + } + } + let s = decode_to_string(encoding, &buf, true) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok(s) + } /// Reads a struct from the reader. /// The struct must implement the `StructUnpack` trait. @@ -663,6 +697,41 @@ pub trait CPeek { CString::new(buf).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } + /// Peeks a fixed-length string from the reader. + fn cpeek_fstring(&self, len: usize, encoding: Encoding, trim: bool) -> Result { + let mut buf = vec![0u8; len]; + self.cpeek_exact(&mut buf)?; + if trim { + let first_zero = buf.iter().position(|&b| b == 0); + if let Some(pos) = first_zero { + buf.truncate(pos); + } + } + let s = decode_to_string(encoding, &buf, true) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok(s) + } + /// Peeks a fixed-length string from the reader at a specific offset. + fn cpeek_fstring_at( + &self, + offset: u64, + len: usize, + encoding: Encoding, + trim: bool, + ) -> Result { + let mut buf = vec![0u8; len]; + self.cpeek_exact_at(offset, &mut buf)?; + if trim { + let first_zero = buf.iter().position(|&b| b == 0); + if let Some(pos) = first_zero { + buf.truncate(pos); + } + } + let s = decode_to_string(encoding, &buf, true) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok(s) + } + /// Peeks data and checks if it matches the provided data. fn cpeek_and_equal(&self, data: &[u8]) -> Result<()> { let mut buf = vec![0u8; data.len()]; @@ -1186,6 +1255,7 @@ impl SeekExt for T { } } +#[derive(Clone)] /// A memory reader that can read data from a vector of bytes. pub struct MemReader { /// The data to read from. @@ -1773,6 +1843,38 @@ impl Result, O: Fn(u64) -> R Ok(()) } + /// Patches a u32 value in the output stream at the specified original offset. + pub fn patch_u32(&mut self, original_offset: u64, value: u32) -> Result<()> { + let input_pos = self.input.stream_position()?; + if input_pos < original_offset + 4 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Original offset is out of bounds for u32 patching", + )); + } + let new_offset = self.map_offset(original_offset)?; + self.output.seek(SeekFrom::Start(new_offset))?; + self.output.write_u32(value)?; + self.output.seek(SeekFrom::End(0))?; + Ok(()) + } + + /// Patches a u32 value in big-endian order in the output stream at the specified original offset. + pub fn patch_u32_be(&mut self, original_offset: u64, value: u32) -> Result<()> { + let input_pos = self.input.stream_position()?; + if input_pos < original_offset + 4 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Original offset is out of bounds for u32 patching", + )); + } + let new_offset = self.map_offset(original_offset)?; + self.output.seek(SeekFrom::Start(new_offset))?; + self.output.write_u32_be(value)?; + self.output.seek(SeekFrom::End(0))?; + Ok(()) + } + /// Patches a u32 address in the output stream at the specified original offset. pub fn patch_u32_address(&mut self, original_offset: u64) -> Result<()> { let input_pos = self.input.stream_position()?; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index cae7cdf..1e5af4b 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -20,6 +20,8 @@ pub mod ex_hibit; pub mod hexen_haus; #[cfg(feature = "kirikiri")] pub mod kirikiri; +#[cfg(feature = "silky")] +pub mod silky; #[cfg(feature = "softpal")] pub mod softpal; #[cfg(feature = "will-plus")] @@ -118,6 +120,8 @@ lazy_static::lazy_static! { Box::new(kirikiri::tjs_ns0::TjsNs0Builder::new()), #[cfg(feature = "kirikiri")] Box::new(kirikiri::tjs2::Tjs2Builder::new()), + #[cfg(feature = "silky")] + Box::new(silky::mes::MesBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/scripts/silky/disasm.rs b/src/scripts/silky/disasm.rs new file mode 100644 index 0000000..89098d6 --- /dev/null +++ b/src/scripts/silky/disasm.rs @@ -0,0 +1,457 @@ +use crate::ext::io::*; +use anyhow::Result; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Oper { + /// Byte + B, + /// Integer + I, + /// Address + A, + /// String + S, + /// Text + T, +} + +use Oper::*; + +pub struct Opcodes { + pub r#yield: u8, + pub add: u8, + pub escape_sequence: u8, + pub message1: u8, + pub message2: u8, + pub push_int: u8, + pub push_string: u8, + pub syscall: u8, + pub line_number: u8, + pub nop1: u8, + pub nop2: u8, + pub is_message1_obfuscated: bool, +} + +pub struct Syscalls { + pub exec: i32, + pub exec_set_character_name: i32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SlikyStringType { + Internal, + Message, + Name, +} + +#[derive(Debug, Clone)] +pub struct SlikyString { + pub start: u64, + pub len: u64, + pub typ: SlikyStringType, +} + +#[derive(Debug, Clone)] +pub enum Obj { + Byte(u8), + Int(i32), + Str(SlikyString), +} + +pub trait Disasm: std::fmt::Debug { + fn stream(&self) -> &MemReader; + fn stream_mut(&mut self) -> &mut MemReader; + fn opcodes(&self) -> &'static Opcodes; + fn operands(&self) -> &'static [(u8, &'static [Oper])]; + fn syscalls(&self) -> &'static [Syscalls]; + fn code_offset(&self) -> u32; + fn big_endian_addresses(&self) -> &[u32]; + fn push_big_endian_addresses(&mut self, addr: u32); + fn little_endian_addresses(&self) -> &[u32]; + fn read_header(&mut self) -> Result<()>; + fn read_instruction(&mut self) -> Result<(u8, Vec)> { + let opcode = self.stream_mut().read_u8()?; + let mut operands = Vec::new(); + if let Some((_, ops)) = self.operands().iter().find(|(op, _)| *op == opcode) { + for &oper in *ops { + operands.push(self.read_operand(oper)?); + } + } + Ok((opcode, operands)) + } + fn read_operand(&mut self, oper: Oper) -> Result { + match oper { + B => Ok(Obj::Byte(self.stream_mut().read_u8()?)), + I => Ok(Obj::Int(self.stream_mut().read_i32_be()?)), + A => { + self.push_big_endian_addresses(self.stream().pos as u32); + Ok(Obj::Int(self.stream_mut().read_i32_be()?)) + } + S | T => { + let start = self.stream().pos as u64; + let s = self.stream_mut().read_cstring()?; + Ok(Obj::Str(SlikyString { + start, + len: s.as_bytes_with_nul().len() as u64, + typ: SlikyStringType::Internal, + })) + } + } + } + fn read_code(&mut self) -> Result> { + let mut stack: Vec = Vec::new(); + let mut message_start_offset = None; + let mut in_ruby = false; + let mut texts = Vec::new(); + self.stream_mut().pos = self.code_offset() as usize; + while !self.stream().is_eof() { + let instr_offset = self.stream().pos as u64; + let (opcode, operands) = self.read_instruction()?; + // message instr + let opcodes = self.opcodes(); + if opcode == opcodes.message1 || opcode == opcodes.message2 { + if message_start_offset.is_none() { + message_start_offset = Some(instr_offset); + } + } else if opcode == opcodes.escape_sequence { + if let Some(Obj::Byte(b)) = operands.get(0) { + if *b == 0x1 { + in_ruby = true; + } + } + } else if opcode == opcodes.r#yield && in_ruby { + in_ruby = false; + } else if opcode == opcodes.push_int + && self.stream().cpeek_u8_at(instr_offset + 5)? == opcodes.line_number + { + // Skip + } else if opcode == opcodes.line_number + || opcode == opcodes.nop1 + || opcode == opcodes.nop2 + { + // Skip + } else { + if let Some(start) = message_start_offset { + let start = start as u64; + let text = SlikyString { + start, + len: instr_offset - start, + typ: SlikyStringType::Message, + }; + texts.push(text); + } + message_start_offset = None; + in_ruby = false; + } + // name instr + if opcode == opcodes.push_int || opcode == opcodes.push_string { + if !operands.is_empty() { + stack.push(operands[0].clone()); + } + } else if opcode == opcodes.add && stack.len() >= 2 { + let value1 = stack.pop().unwrap(); + let value2 = stack.pop().unwrap(); + if let (Obj::Int(i1), Obj::Int(i2)) = (value1, value2) { + stack.push(Obj::Int(i1 + i2)); + } + } else if opcode == opcodes.syscall && stack.len() >= 3 { + let func_id = stack.pop().unwrap(); + let exec_id = stack.pop().unwrap(); + let name = stack.pop().unwrap(); + if let (Obj::Int(func_id), Obj::Int(exec_id), Obj::Str(name)) = + (func_id, exec_id, name) + { + for syscall in self.syscalls() { + if func_id == syscall.exec && exec_id == syscall.exec_set_character_name { + texts.push(SlikyString { + start: name.start - 1, + len: name.len + 1, + typ: SlikyStringType::Name, + }); + } + } + } + stack.clear(); + } else { + stack.clear(); + } + } + Ok(texts) + } +} + +pub const PLUS_OPCODES: Opcodes = Opcodes { + r#yield: 0x00, + add: 0x34, + escape_sequence: 0x1c, + message1: 0x0a, + message2: 0x0b, + push_int: 0x32, + push_string: 0x33, + syscall: 0x18, + line_number: 0xff, + nop1: 0xfc, + nop2: 0xfd, + is_message1_obfuscated: true, +}; + +const PLUS_OPERANDS: [(u8, &[Oper]); 53] = [ + (0x00, &[]), // yield + (0x01, &[]), // ret + (0x02, &[]), // ldglob1.i8 + (0x03, &[]), // ldglob2.i16 + (0x04, &[]), // ldglob3.var + (0x05, &[]), // ldglob4.var + (0x06, &[]), // ldloc.var + (0x07, &[]), // ldglob5.i8 + (0x08, &[]), // ldglob5.i16 + (0x09, &[]), // ldglob5.i32 + (0x0A, &[S]), // message + (0x0B, &[T]), // message + (0x0C, &[]), // stglob1.i8 + (0x0D, &[]), // stglob2.i16 + (0x0E, &[]), // stglob3.var + (0x0F, &[]), // stglob4.var + (0x10, &[]), // stloc.var + (0x11, &[]), // stglob5.i8 + (0x12, &[]), // stglob5.i16 + (0x13, &[]), // stglob5.i32 + (0x14, &[A]), // jz + (0x15, &[A]), // jmp + (0x16, &[A]), // libreg + (0x17, &[]), // libcall + (0x18, &[]), // syscall + (0x19, &[I]), // msgid + (0x1A, &[I]), // msgid2 + (0x1B, &[A]), // choice + (0x1C, &[B]), // escape sequence + (0x32, &[I]), // ldc.i4 + (0x33, &[S]), // ldstr + (0x34, &[]), // add + (0x35, &[]), // sub + (0x36, &[]), // mul + (0x37, &[]), // div + (0x38, &[]), // mod + (0x39, &[]), // rand + (0x3A, &[]), // logand + (0x3B, &[]), // logor + (0x3C, &[]), // binand + (0x3D, &[]), // binor + (0x3E, &[]), // lt + (0x3F, &[]), // gt + (0x40, &[]), // le + (0x41, &[]), // ge + (0x42, &[]), // eq + (0x43, &[]), // neq + (0xFA, &[]), + (0xFB, &[]), + (0xFC, &[]), + (0xFD, &[]), + (0xFE, &[]), + (0xFF, &[]), +]; + +const PLUS_SYSCALLS: [Syscalls; 2] = [ + Syscalls { + exec: 29, + exec_set_character_name: 11, + }, + Syscalls { + exec: 29, + exec_set_character_name: 15, + }, +]; + +#[derive(Debug)] +pub struct PlusDisasm { + stream: MemReader, + num_messages: u32, + num_special_messages: u32, + code_offset: u32, + big_endian_addresses: Vec, + little_endian_addresses: Vec, +} + +impl PlusDisasm { + pub fn new(mut stream: MemReader) -> Result { + let num_messages = stream.read_u32()?; + let num_special_messages = stream.read_u32()?; + let code_offset = 8 + (num_messages + num_special_messages) * 4; + Ok(Self { + stream, + num_messages, + num_special_messages, + code_offset, + big_endian_addresses: Vec::new(), + little_endian_addresses: Vec::new(), + }) + } +} + +impl Disasm for PlusDisasm { + fn stream(&self) -> &MemReader { + &self.stream + } + fn stream_mut(&mut self) -> &mut MemReader { + &mut self.stream + } + fn opcodes(&self) -> &'static Opcodes { + &PLUS_OPCODES + } + fn operands(&self) -> &'static [(u8, &'static [Oper])] { + &PLUS_OPERANDS + } + fn syscalls(&self) -> &'static [Syscalls] { + &PLUS_SYSCALLS + } + fn code_offset(&self) -> u32 { + self.code_offset + } + fn big_endian_addresses(&self) -> &[u32] { + &self.big_endian_addresses + } + fn push_big_endian_addresses(&mut self, addr: u32) { + self.big_endian_addresses.push(addr); + } + fn little_endian_addresses(&self) -> &[u32] { + &self.little_endian_addresses + } + fn read_header(&mut self) -> Result<()> { + for i in 0..self.num_messages + self.num_special_messages { + self.little_endian_addresses.push(8 + i * 4); + } + self.stream.pos = self.code_offset as usize; + Ok(()) + } +} + +const AI6_WIN_OPCODES: Opcodes = Opcodes { + r#yield: 0x00, + add: 0x34, + escape_sequence: 0x1b, + message1: 0x0a, + message2: 0x0b, + push_int: 0x32, + push_string: 0x33, + syscall: 0x18, + line_number: 0xff, + nop1: 0xfc, + nop2: 0xfd, + is_message1_obfuscated: false, +}; + +const AI6_WIN_OPERANDS: [(u8, &[Oper]); 48] = [ + (0x00, &[]), // yield + (0x01, &[]), // ret + (0x02, &[]), // ldglob1.i8 + (0x03, &[]), // ldglob2.i16 + (0x04, &[]), // ldglob3.var + (0x05, &[]), // ldglob4.var + (0x06, &[]), // ldloc.var + (0x07, &[]), // ldglob5.i8 + (0x08, &[]), // ldglob5.i16 + (0x09, &[]), // ldglob5.i32 + (0x0A, &[S]), // message + (0x0B, &[S]), // message + (0x0C, &[]), // stglob1.i8 + (0x0D, &[]), // stglob2.i16 + (0x0E, &[]), // stglob3.var + (0x0F, &[]), // stglob4.var + (0x10, &[]), // stloc.var + (0x11, &[]), // stglob5.i8 + (0x12, &[]), // stglob5.i16 + (0x13, &[]), // stglob5.i32 + (0x14, &[A]), // jz + (0x15, &[A]), // jmp + (0x16, &[A]), // libreg + (0x17, &[]), // libcall + (0x18, &[]), // syscall + (0x19, &[I]), // msgid + (0x1A, &[A]), // choice + (0x1B, &[B]), // escape sequence + (0x32, &[I]), // ldc.i4 + (0x33, &[S]), // ldstr + (0x34, &[]), // add + (0x35, &[]), // sub + (0x36, &[]), // mul + (0x37, &[]), // div + (0x38, &[]), // mod + (0x39, &[]), // rand + (0x3A, &[]), // logand + (0x3B, &[]), // logor + (0x3C, &[]), // binand + (0x3D, &[]), // binor + (0x3E, &[]), // lt + (0x3F, &[]), // gt + (0x40, &[]), // le + (0x41, &[]), // ge + (0x42, &[]), // eq + (0x43, &[]), // neq + (0xFE, &[]), + (0xFF, &[]), +]; + +const AI6_WIN_SYSCALLS: [Syscalls; 1] = [Syscalls { + exec: 31, + exec_set_character_name: 15, +}]; + +#[derive(Debug)] +pub struct Ai6WinDisasm { + stream: MemReader, + num_messages: u32, + code_offset: u32, + big_endian_addresses: Vec, + little_endian_addresses: Vec, +} + +impl Ai6WinDisasm { + pub fn new(mut stream: MemReader) -> Result { + let num_messages = stream.read_u32()?; + let code_offset = 4 + num_messages * 4; + Ok(Self { + stream, + num_messages, + code_offset, + big_endian_addresses: Vec::new(), + little_endian_addresses: Vec::new(), + }) + } +} + +impl Disasm for Ai6WinDisasm { + fn stream(&self) -> &MemReader { + &self.stream + } + fn stream_mut(&mut self) -> &mut MemReader { + &mut self.stream + } + fn opcodes(&self) -> &'static Opcodes { + &AI6_WIN_OPCODES + } + fn operands(&self) -> &'static [(u8, &'static [Oper])] { + &AI6_WIN_OPERANDS + } + fn syscalls(&self) -> &'static [Syscalls] { + &AI6_WIN_SYSCALLS + } + fn code_offset(&self) -> u32 { + self.code_offset + } + fn big_endian_addresses(&self) -> &[u32] { + &self.big_endian_addresses + } + fn push_big_endian_addresses(&mut self, addr: u32) { + self.big_endian_addresses.push(addr); + } + fn little_endian_addresses(&self) -> &[u32] { + &self.little_endian_addresses + } + fn read_header(&mut self) -> Result<()> { + for i in 0..self.num_messages { + self.little_endian_addresses.push(4 + i * 4); + } + self.stream.pos = self.code_offset as usize; + Ok(()) + } +} diff --git a/src/scripts/silky/mes.rs b/src/scripts/silky/mes.rs new file mode 100644 index 0000000..ba3dd7c --- /dev/null +++ b/src/scripts/silky/mes.rs @@ -0,0 +1,426 @@ +use super::disasm::*; +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use anyhow::Result; +use std::cell::RefCell; +use std::io::Write; +use unicode_segmentation::UnicodeSegmentation; + +#[derive(Debug)] +/// Sliky mes script builder +pub struct MesBuilder {} + +impl MesBuilder { + /// Create a new Sliky mes script builder + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for MesBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(Mes::new(buf, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["mes"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::Silky + } +} + +struct TextParser<'a> { + data: Vec<&'a str>, + typ: SlikyStringType, + opcodes: &'static Opcodes, + encoding: Encoding, + pos: usize, +} + +impl<'a> TextParser<'a> { + fn new( + s: &'a str, + typ: SlikyStringType, + opcodes: &'static Opcodes, + encoding: Encoding, + ) -> Self { + let data = s.graphemes(true).collect(); + Self { + data, + typ, + opcodes, + encoding, + pos: 0, + } + } + + fn parse(mut self) -> Result> { + match self.typ { + SlikyStringType::Internal => Err(anyhow::anyhow!( + "Internal strings cannot be parsed from text." + )), + SlikyStringType::Name => { + let mut m = MemWriter::new(); + m.write_u8(self.opcodes.push_string)?; + let s = encode_string(self.encoding, &self.data.join(""), false)?; + m.write_all(&s)?; + m.write_u8(0)?; + Ok(m.into_inner()) + } + SlikyStringType::Message => { + let mut m = MemWriter::new(); + let mut in_ruby = false; + let mut in_normal_text = false; + while let Some(c) = self.next() { + match c { + "[" => { + if in_ruby { + return Err(anyhow::anyhow!("Nested ruby tags are not allowed.")); + } + if in_normal_text { + m.write_u8(0)?; + in_normal_text = false; + } + in_ruby = true; + m.write_u8(self.opcodes.escape_sequence)?; + m.write_u8(1)?; // ruby start + m.write_u8(self.opcodes.message2)?; + } + "]" => { + if !in_ruby { + return Err(anyhow::anyhow!("Unmatched closing ruby tag.")); + } + in_ruby = false; + m.write_u8(0)?; + m.write_u8(self.opcodes.r#yield)?; + } + "\n" => { + if in_ruby { + return Err(anyhow::anyhow!("Newline inside ruby is not allowed.")); + } + if in_normal_text { + m.write_u8(0)?; + in_normal_text = false; + } + m.write_u8(self.opcodes.escape_sequence)?; + m.write_u8(0)?; // new line + } + _ => { + if !in_ruby && !in_normal_text { + in_normal_text = true; + m.write_u8(self.opcodes.message2)?; + } + let s = encode_string(self.encoding, c, false)?; + m.write_all(&s)?; + } + } + } + if in_ruby { + m.write_u8(0)?; + m.write_u8(self.opcodes.r#yield)?; + } + if in_normal_text { + m.write_u8(0)?; + } + Ok(m.into_inner()) + } + } + } + + fn next(&mut self) -> Option<&'a str> { + if self.pos < self.data.len() { + let c = self.data[self.pos]; + self.pos += 1; + Some(c) + } else { + None + } + } +} + +#[derive(Debug)] +pub struct Mes { + disasm: RefCell>, + encoding: Encoding, + texts: Vec, +} + +impl Mes { + pub fn new(buf: Vec, encoding: Encoding, _config: &ExtraConfig) -> Result { + let reader = MemReader::new(buf); + let num_message = reader.cpeek_u32()?; + let code_offset = 4 + num_message as u64 * 4; + let first_line_offset = reader.cpeek_u32_at(4)? as u64 + code_offset; + let mut disasm: Box = if reader.cpeek_u8_at(first_line_offset)? == 0x19 + && reader.cpeek_u32_at(first_line_offset + 1)? == 0 + { + Box::new(Ai6WinDisasm::new(reader)?) + } else { + Box::new(PlusDisasm::new(reader)?) + }; + disasm.read_header()?; + let texts = disasm.read_code()?; + Ok(Self { + disasm: RefCell::new(disasm), + encoding, + texts, + }) + } + + fn code_to_text(&self, str: &SlikyString) -> Result { + let mut disasm = self.disasm.try_borrow_mut()?; + let mut result = String::new(); + disasm.stream_mut().pos = str.start as usize; + let end = str.start as usize + str.len as usize; + let opcodes = disasm.opcodes(); + while disasm.stream().pos < end { + let (opcode, operands) = disasm.read_instruction()?; + if opcode == opcodes.push_string + || (opcode == opcodes.message1 && !opcodes.is_message1_obfuscated) + || opcode == opcodes.message2 + { + if let Some(Obj::Str(s)) = operands.get(0) { + let s = disasm.stream().cpeek_fstring_at( + s.start, + s.len as usize, + self.encoding, + true, + )?; + result.push_str(&s); + } + } else if opcode == opcodes.message1 && opcodes.is_message1_obfuscated { + if let Some(Obj::Str(s)) = operands.get(0) { + let mut deobfuscated = vec![0u8; (s.len as usize - 1) * 2]; + let mut input_idx = 0; + let mut output_idx = 0; + let tlen = s.len - 1; + while input_idx < tlen { + let b = disasm.stream().cpeek_u8_at(s.start + input_idx)?; + input_idx += 1; + if matches!(b, 0x81..0xA0 | 0xE0..0xF0) { + deobfuscated[output_idx] = b; + output_idx += 1; + deobfuscated[output_idx] = + disasm.stream().cpeek_u8_at(s.start + input_idx)?; + input_idx += 1; + output_idx += 1; + } else { + let c = b as i32 - 0x7D62; + deobfuscated[output_idx] = (c >> 8) as u8; + output_idx += 1; + deobfuscated[output_idx] = (c & 0xFF) as u8; + output_idx += 1; + } + } + deobfuscated.truncate(output_idx); + let s = decode_to_string(self.encoding, &deobfuscated, true)?; + result.push_str(&s); + } + } else if opcode == opcodes.escape_sequence { + if let Some(Obj::Byte(e)) = operands.get(0) { + match e { + // new line + 0 => result.push('\n'), + // ruby + 1 => result.push_str("["), + _ => { + return Err(anyhow::anyhow!("Unknown escape sequence: {}", e)); + } + } + } + } else if opcode == opcodes.r#yield { + result.push_str("]"); + } + } + Ok(result) + } +} + +impl Script for Mes { + 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; + for t in self.texts.iter() { + match t.typ { + SlikyStringType::Internal => {} + SlikyStringType::Name => { + name = Some(self.code_to_text(t)?); + } + SlikyStringType::Message => { + let message = self.code_to_text(t)?; + messages.push(Message { + name: name.take(), + message, + }); + } + } + } + Ok(messages) + } + + fn import_messages<'a>( + &'a self, + messages: Vec, + file: Box, + _filename: &str, + encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + let opcodes = self.disasm.try_borrow()?.opcodes(); + let mut inp = self.disasm.try_borrow()?.stream().clone(); + inp.pos = 0; + let mut patcher = BinaryPatcher::new(inp.to_ref(), file, |add| Ok(add), |add| Ok(add))?; + let mut mess = messages.iter(); + let mut mes = mess.next(); + for text in &self.texts { + patcher.copy_up_to(text.start)?; + match text.typ { + // Ignore internal strings + SlikyStringType::Internal => {} + SlikyStringType::Name => { + let m = match mes { + Some(m) => m, + None => { + return Err(anyhow::anyhow!("Not enough messages")); + } + }; + let mut name = match &m.name { + Some(n) => n.to_string(), + None => { + return Err(anyhow::anyhow!("Message name is missing")); + } + }; + if let Some(repl) = replacement { + for (k, v) in &repl.map { + name = name.replace(k, v); + } + } + let data = + TextParser::new(&name, SlikyStringType::Name, opcodes, encoding).parse()?; + patcher.replace_bytes(text.len, &data)?; + } + SlikyStringType::Message => { + let m = match mes { + Some(m) => m, + None => { + return Err(anyhow::anyhow!("Not enough messages")); + } + }; + let mut message = m.message.to_string(); + if let Some(repl) = replacement { + for (k, v) in &repl.map { + message = message.replace(k, v); + } + } + let data = + TextParser::new(&message, SlikyStringType::Message, opcodes, encoding) + .parse()?; + patcher.replace_bytes(text.len, &data)?; + mes = mess.next(); + } + } + } + if mes.is_some() || mess.next().is_some() { + return Err(anyhow::anyhow!("Too many messages")); + } + patcher.copy_up_to(inp.data.len() as u64)?; + let code_offset = self.disasm.try_borrow()?.code_offset(); + for &address_offset in self.disasm.try_borrow()?.little_endian_addresses() { + let orig_address = inp.cpeek_u32_at(address_offset as u64)? as u64; + let orig_offset = orig_address + code_offset as u64; + let new_offset = patcher.map_offset(orig_offset)?; + let new_address = new_offset - code_offset as u64; + patcher.patch_u32(address_offset as u64, new_address as u32)?; + } + for &address_offset in self.disasm.try_borrow()?.big_endian_addresses() { + let orig_address = inp.cpeek_u32_be_at(address_offset as u64)? as u64; + let orig_offset = orig_address + code_offset as u64; + let new_offset = patcher.map_offset(orig_offset)?; + let new_address = new_offset - code_offset as u64; + patcher.patch_u32_be(address_offset as u64, new_address as u32)?; + } + Ok(()) + } +} + +#[test] +fn test_text_parser() { + let opcodes = &PLUS_OPCODES; + let parser = TextParser::new( + "Hello, [world]s\nThis is a test.", + SlikyStringType::Message, + opcodes, + Encoding::Utf8, + ); + let data = parser.parse().unwrap(); + assert_eq!( + data, + vec![ + opcodes.message2, + b'H', + b'e', + b'l', + b'l', + b'o', + b',', + b' ', + 0, + opcodes.escape_sequence, + 1, + opcodes.message2, + b'w', + b'o', + b'r', + b'l', + b'd', + 0, + opcodes.r#yield, + opcodes.message2, + b's', + 0, + opcodes.escape_sequence, + 0, + opcodes.message2, + b'T', + b'h', + b'i', + b's', + b' ', + b'i', + b's', + b' ', + b'a', + b' ', + b't', + b'e', + b's', + b't', + b'.', + 0 + ] + ); +} diff --git a/src/scripts/silky/mod.rs b/src/scripts/silky/mod.rs new file mode 100644 index 0000000..8e0d639 --- /dev/null +++ b/src/scripts/silky/mod.rs @@ -0,0 +1,2 @@ +mod disasm; +pub mod mes; diff --git a/src/types.rs b/src/types.rs index 8a8f505..1db1e77 100644 --- a/src/types.rs +++ b/src/types.rs @@ -535,6 +535,9 @@ pub enum ScriptType { #[value(alias("kr-tjs-ns0"))] /// Kirikiri TJS NS0 binary encoded script KirikiriTjsNs0, + #[cfg(feature = "silky")] + /// Silky Engine Mes script + Silky, #[cfg(feature = "softpal")] /// Softpal src script Softpal,