From a4aafb089e2fdfff1d76ca49fac1655ed200ad97 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sun, 14 Sep 2025 20:18:29 +0800 Subject: [PATCH] Add silky engine map file support fix output extension for custom when importing --- README.md | 1 + src/ext/io.rs | 104 +++++++++++++++ src/main.rs | 6 +- src/scripts/mod.rs | 2 + src/scripts/silky/map.rs | 270 +++++++++++++++++++++++++++++++++++++++ src/scripts/silky/mod.rs | 1 + src/types.rs | 3 + 7 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 src/scripts/silky/map.rs diff --git a/README.md b/README.md index af31952..de15d28 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ msg-tool create -t | Script Type | Feature Name | Name | Export | Import | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---| | `silky` | `silky` | Silky Engine Mes Script File (.mes) | ✔️ | ✔️ | ❌ | ❌ | ❌ | | +| `silky-map` | `silky` | Silky Engine Map File (.map) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | ### 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 a4bcf8d..a5c33dc 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -294,6 +294,13 @@ pub trait Peek { Ok(s) } + /// Peeks a UTF-16 string (null-terminated) from the reader. + /// Returns the raw bytes of the UTF-16 string. (Null terminator is not included) + fn peek_u16string(&mut self) -> Result>; + /// Peeks a UTF-16 string (null-terminated) from the reader at a specific offset. + /// Returns the raw bytes of the UTF-16 string. (Null terminator is not included) + fn peek_u16string_at(&mut self, offset: u64) -> Result>; + /// Reads a struct from the reader. /// The struct must implement the `StructUnpack` trait. /// @@ -409,6 +416,37 @@ impl Peek for T { CString::new(buf).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } + fn peek_u16string(&mut self) -> Result> { + let current_pos = self.stream_position()?; + let mut buf = Vec::new(); + loop { + let mut bytes = [0u8; 2]; + self.read_exact(&mut bytes)?; + if bytes == [0, 0] { + break; + } + buf.extend_from_slice(&bytes); + } + self.seek(SeekFrom::Start(current_pos))?; + Ok(buf) + } + + fn peek_u16string_at(&mut self, offset: u64) -> Result> { + let current_pos = self.stream_position()?; + let mut buf = Vec::new(); + self.seek(SeekFrom::Start(offset as u64))?; + loop { + let mut bytes = [0u8; 2]; + self.read_exact(&mut bytes)?; + if bytes == [0, 0] { + break; + } + buf.extend_from_slice(&bytes); + } + self.seek(SeekFrom::Start(current_pos))?; + Ok(buf) + } + fn read_struct(&mut self, big: bool, encoding: Encoding) -> Result { S::unpack(self, big, encoding) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) @@ -732,6 +770,26 @@ pub trait CPeek { Ok(s) } + /// Peeks a UTF-16 string (null-terminated) from the reader. + /// Returns the raw bytes of the UTF-16 string. (Null terminator is not included) + fn cpeek_u16string(&self) -> Result>; + /// Peeks a UTF-16 string (null-terminated) from the reader at a specific offset. + /// Returns the raw bytes of the UTF-16 string. (Null terminator is not + fn cpeek_u16string_at(&self, offset: u64) -> Result> { + let mut buf = Vec::new(); + let mut bytes = [0u8; 2]; + let mut current_offset = offset; + loop { + self.cpeek_exact_at(current_offset, &mut bytes)?; + if bytes == [0, 0] { + break; + } + buf.extend_from_slice(&bytes); + current_offset += 2; + } + Ok(buf) + } + /// 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()]; @@ -779,6 +837,13 @@ impl CPeek for Mutex { })?; lock.peek_cstring() } + + fn cpeek_u16string(&self) -> Result> { + let mut lock = self.lock().map_err(|_| { + std::io::Error::new(std::io::ErrorKind::Other, "Failed to lock the mutex") + })?; + lock.peek_u16string() + } } /// A trait to help to read data from a reader. @@ -836,6 +901,10 @@ pub trait ReadExt { /// * `trim` indicates whether to trim the string after the first null byte. fn read_fstring(&mut self, len: usize, encoding: Encoding, trim: bool) -> Result; + /// Reads a UTF-16 string (null-terminated) from the reader. + /// Returns the raw bytes of the UTF-16 string. (Null terminator is not included) + fn read_u16string(&mut self) -> Result>; + /// Reads some data from the reader into a vector. fn read_exact_vec(&mut self, len: usize) -> Result>; @@ -981,6 +1050,19 @@ impl ReadExt for T { Ok(s) } + fn read_u16string(&mut self) -> Result> { + let mut buf = Vec::new(); + loop { + let mut bytes = [0u8; 2]; + self.read_exact(&mut bytes)?; + if bytes == [0, 0] { + break; + } + buf.extend_from_slice(&bytes); + } + Ok(buf) + } + fn read_exact_vec(&mut self, len: usize) -> Result> { let mut buf = vec![0u8; len]; self.read_exact(&mut buf)?; @@ -1399,6 +1481,10 @@ impl CPeek for MemReader { fn cpeek_cstring(&self) -> Result { self.to_ref().cpeek_cstring() } + + fn cpeek_u16string(&self) -> Result> { + self.to_ref().cpeek_u16string() + } } impl<'a> Read for MemReaderRef<'a> { @@ -1489,6 +1575,20 @@ impl<'a> CPeek for MemReaderRef<'a> { } CString::new(buf).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } + + fn cpeek_u16string(&self) -> Result> { + let mut buf = Vec::new(); + let mut i = self.pos; + while i + 1 < self.data.len() { + let bytes = &self.data[i..i + 2]; + if bytes == [0, 0] { + break; + } + buf.extend_from_slice(bytes); + i += 2; + } + Ok(buf) + } } /// A memory writer that can write data to a vector of bytes. @@ -1612,6 +1712,10 @@ impl CPeek for MemWriter { fn cpeek_cstring(&self) -> Result { self.to_ref().cpeek_cstring() } + + fn cpeek_u16string(&self) -> Result> { + self.to_ref().cpeek_u16string() + } } /// A region of a stream that can be read/write and seeked within a specified range. diff --git a/src/main.rs b/src/main.rs index 86e0ead..3b047fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1504,7 +1504,11 @@ pub fn import_script( if arg.output_no_extra_ext { pb.remove_all_extensions(); } - pb.set_extension(of.as_ref()); + pb.set_extension(if of.is_custom() { + script.custom_output_extension() + } else { + of.as_ref() + }); pb.to_string_lossy().into_owned() } else { imp_cfg.output.clone() diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 69ff03b..0d8f224 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -126,6 +126,8 @@ lazy_static::lazy_static! { Box::new(silky::mes::MesBuilder::new()), #[cfg(feature = "favorite")] Box::new(favorite::hcb::HcbScriptBuilder::new()), + #[cfg(feature = "silky")] + Box::new(silky::map::MapBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/scripts/silky/map.rs b/src/scripts/silky/map.rs new file mode 100644 index 0000000..cb496f5 --- /dev/null +++ b/src/scripts/silky/map.rs @@ -0,0 +1,270 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use anyhow::Result; +use std::io::{Seek, SeekFrom}; + +#[derive(Debug)] +/// A builder for Silky Engine map scripts. +pub struct MapBuilder {} + +impl MapBuilder { + /// Creates a new `MapBuilder`. + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for MapBuilder { + 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(Map::new(buf, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["map"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::SilkyMap + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + let reader = MemReaderRef::new(&buf[..buf_len]); + try_parse(reader).ok() + } + + fn can_create_file(&self) -> bool { + true + } + + fn create_file<'a>( + &'a self, + filename: &'a str, + writer: Box, + encoding: Encoding, + file_encoding: Encoding, + config: &ExtraConfig, + ) -> Result<()> { + create_file( + filename, + writer, + encoding, + file_encoding, + config.custom_yaml, + ) + } +} + +fn create_file<'a>( + custom_filename: &'a str, + mut writer: Box, + encoding: Encoding, + output_encoding: Encoding, + yaml: bool, +) -> Result<()> { + let input = crate::utils::files::read_file(custom_filename)?; + let s = decode_to_string(output_encoding, &input, true)?; + let strings: Vec = if yaml { + serde_yaml_ng::from_str(&s)? + } else { + serde_json::from_str(&s)? + }; + writer.write_u32(strings.len() as u32)?; + let header_len = 8 * strings.len(); + writer.seek_relative(header_len as i64)?; + let mut offsets = Vec::with_capacity(strings.len()); + for s in strings { + offsets.push(writer.stream_position()? as u32); + let buf = if encoding.is_utf16le() { + let mut buf = encode_string(encoding, &s, false)?; + buf.extend_from_slice(&[0, 0]); + buf + } else { + let mut buf = encode_string(encoding, &s, false)?; + buf.push(0); + buf + }; + writer.write_all(&buf)?; + } + writer.seek(SeekFrom::Start(4))?; + for (i, offset) in offsets.iter().enumerate() { + writer.write_u32(i as u32)?; + writer.write_u32(*offset)?; + } + Ok(()) +} + +#[derive(Debug)] +/// A Silky Engine map script. +struct Map { + strings: Vec, + custom_yaml: bool, +} + +impl Map { + /// Creates a new `Map` from the given buffer and encoding. + pub fn new(buf: Vec, encoding: Encoding, config: &ExtraConfig) -> Result { + let mut data = MemReader::new(buf); + let count = data.read_u32()?; + let mut strings = Vec::with_capacity(count as usize); + for _ in 0..count { + let _index = data.read_u32()?; + let offset = data.read_u32()?; + if encoding.is_utf16le() { + let data = data.peek_u16string_at(offset as u64)?; + let s = decode_to_string(encoding, &data, true)?; + strings.push(s); + } else { + let data = data.peek_cstring_at(offset as u64)?; + let s = decode_to_string(encoding, data.as_bytes(), true)?; + strings.push(s); + } + } + Ok(Self { + strings, + custom_yaml: config.custom_yaml, + }) + } +} + +impl Script for Map { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn is_output_supported(&self, _: OutputScriptType) -> bool { + true + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + 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::with_capacity(self.strings.len()); + for s in &self.strings { + messages.push(Message::new(s.replace("\\n", "\n"), None)); + } + Ok(messages) + } + + fn import_messages<'a>( + &'a self, + messages: Vec, + mut file: Box, + _filename: &str, + encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + if messages.len() != self.strings.len() { + return Err(anyhow::anyhow!( + "The number of messages does not match. (expected {}, got {})", + self.strings.len(), + messages.len() + )); + } + file.write_u32(messages.len() as u32)?; + let header_len = 8 * messages.len(); + file.seek_relative(header_len as i64)?; + let mut offsets = Vec::with_capacity(messages.len()); + for msg in messages { + let mut m = msg.message.clone(); + if let Some(table) = replacement { + for (k, v) in &table.map { + m = m.replace(k, v); + } + } + m = m.replace("\n", "\\n"); + offsets.push(file.stream_position()? as u32); + let buf = if encoding.is_utf16le() { + let mut buf = encode_string(encoding, &m, false)?; + buf.extend_from_slice(&[0, 0]); + buf + } else { + let mut buf = encode_string(encoding, &m, false)?; + buf.push(0); + buf + }; + file.write_all(&buf)?; + } + file.seek(SeekFrom::Start(4))?; + for (i, offset) in offsets.iter().enumerate() { + file.write_u32(i as u32)?; + file.write_u32(*offset)?; + } + Ok(()) + } + + fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { + let s = if self.custom_yaml { + serde_yaml_ng::to_string(&self.strings)? + } else { + serde_json::to_string_pretty(&self.strings)? + }; + let s = encode_string(encoding, &s, false)?; + let mut file = crate::utils::files::write_file(filename)?; + file.write_all(&s)?; + Ok(()) + } + + fn custom_import<'a>( + &'a self, + custom_filename: &'a str, + file: Box, + encoding: Encoding, + output_encoding: Encoding, + ) -> Result<()> { + create_file( + custom_filename, + file, + encoding, + output_encoding, + self.custom_yaml, + ) + } +} + +fn try_parse(mut r: MemReaderRef) -> Result { + let count = r.read_u32()?; + let index = r.read_u32()?; + if index != 0 { + return Err(anyhow::anyhow!("Invalid index")); + } + let mut prv_offset = r.read_u32()?; + if prv_offset < 4 + 8 * count { + return Err(anyhow::anyhow!("Invalid offset")); + } + let tlen = r.data.len(); + for i in 1..count { + if r.pos + 8 > tlen { + break; + } + let index = r.read_u32()?; + if index != i { + return Err(anyhow::anyhow!("Invalid index")); + } + let offset = r.read_u32()?; + if offset <= prv_offset { + return Err(anyhow::anyhow!("Invalid offset")); + } + prv_offset = offset; + } + Ok(if (r.pos - 4) / 8 < 100 { 10 } else { 20 }) +} diff --git a/src/scripts/silky/mod.rs b/src/scripts/silky/mod.rs index 8e0d639..65c629a 100644 --- a/src/scripts/silky/mod.rs +++ b/src/scripts/silky/mod.rs @@ -1,2 +1,3 @@ mod disasm; +pub mod map; pub mod mes; diff --git a/src/types.rs b/src/types.rs index ced7f32..82fe121 100644 --- a/src/types.rs +++ b/src/types.rs @@ -592,6 +592,9 @@ pub enum ScriptType { #[cfg(feature = "silky")] /// Silky Engine Mes script Silky, + #[cfg(feature = "silky")] + /// Silky Engine Map script + SilkyMap, #[cfg(feature = "softpal")] /// Softpal src script Softpal,