From dac8aaae859ecc046baf6ddd7187083ab758d792 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 9 Jun 2025 23:01:30 +0800 Subject: [PATCH] =?UTF-8?q?Add=20itufuru(=E3=81=84=E3=81=A4=E3=81=8B?= =?UTF-8?q?=E9=99=8D=E3=82=8B=E9=9B=AA)=20script=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 4 +- src/main.rs | 21 ++ src/scripts/base.rs | 7 +- src/scripts/mod.rs | 6 + src/scripts/yaneurao/itufuru/archive.rs | 412 ++++++++++++++++++++++++ src/scripts/yaneurao/itufuru/crypto.rs | 59 ++++ src/scripts/yaneurao/itufuru/mod.rs | 3 + src/scripts/yaneurao/itufuru/script.rs | 164 ++++++++++ src/scripts/yaneurao/mod.rs | 2 + src/types.rs | 8 + 10 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 src/scripts/yaneurao/itufuru/archive.rs create mode 100644 src/scripts/yaneurao/itufuru/crypto.rs create mode 100644 src/scripts/yaneurao/itufuru/mod.rs create mode 100644 src/scripts/yaneurao/itufuru/script.rs create mode 100644 src/scripts/yaneurao/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 5145b38..56e4336 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,11 +17,13 @@ serde_json = "1" unicode-segmentation = "1.12" [features] -default = ["bgi", "circus", "escude", "escude-arc"] +default = ["bgi", "circus", "escude", "escude-arc", "yaneurao", "yaneurao-itufuru"] bgi = [] circus = [] escude = ["int-enum"] escude-arc = ["escude", "rand"] +yaneurao = [] +yaneurao-itufuru = ["yaneurao"] [target.'cfg(windows)'.dependencies] windows-sys = { version = "0", features = ["Win32_Globalization", "Win32_System_Diagnostics_Debug"] } diff --git a/src/main.rs b/src/main.rs index 8eeff02..993459c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -242,6 +242,27 @@ pub fn parse_script_from_archive( Box, &'static Box, )> { + match file.script_type() { + Some(typ) => { + for builder in scripts::BUILDER.iter() { + if typ == builder.script_type() { + let encoding = get_encoding(arg, builder); + let archive_encoding = get_archived_encoding(arg, builder, encoding); + return Ok(( + builder.build_script( + file.data().to_vec(), + file.name(), + encoding, + archive_encoding, + config, + )?, + builder, + )); + } + } + } + _ => {} + } let mut exts_builder = Vec::new(); for builder in scripts::BUILDER.iter() { let exts = builder.extensions(); diff --git a/src/scripts/base.rs b/src/scripts/base.rs index 21ab892..2550c30 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -112,7 +112,12 @@ pub trait ScriptBuilder: std::fmt::Debug { pub trait ArchiveContent { fn name(&self) -> &str; fn data(&self) -> &[u8]; - fn is_script(&self) -> bool; + fn is_script(&self) -> bool { + self.script_type().is_some() + } + fn script_type(&self) -> Option<&ScriptType> { + None + } } pub trait Script: std::fmt::Debug { diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 12b1f9e..071faeb 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -5,6 +5,8 @@ pub mod bgi; pub mod circus; #[cfg(feature = "escude")] pub mod escude; +#[cfg(feature = "yaneurao")] +pub mod yaneurao; pub use base::{Script, ScriptBuilder}; @@ -24,6 +26,10 @@ lazy_static::lazy_static! { Box::new(escude::script::EscudeBinScriptBuilder::new()), #[cfg(feature = "escude")] Box::new(escude::list::EscudeBinListBuilder::new()), + #[cfg(feature = "yaneurao-itufuru")] + Box::new(yaneurao::itufuru::script::ItufuruScriptBuilder::new()), + #[cfg(feature = "yaneurao-itufuru")] + Box::new(yaneurao::itufuru::archive::ItufuruArchiveBuilder::new()), ]; pub static ref ALL_EXTS: Vec = BUILDER.iter().flat_map(|b| b.extensions()).map(|s| s.to_string()).collect(); diff --git a/src/scripts/yaneurao/itufuru/archive.rs b/src/scripts/yaneurao/itufuru/archive.rs new file mode 100644 index 0000000..43a4fc9 --- /dev/null +++ b/src/scripts/yaneurao/itufuru/archive.rs @@ -0,0 +1,412 @@ +use super::crypto::*; +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::encode_string; +use crate::utils::struct_pack::*; +use anyhow::Result; +use msg_tool_macro::*; +use std::collections::HashMap; +use std::io::{Read, Seek, SeekFrom, Write}; + +#[derive(Debug)] +pub struct ItufuruArchiveBuilder {} + +impl ItufuruArchiveBuilder { + pub const fn new() -> Self { + ItufuruArchiveBuilder {} + } +} + +impl ScriptBuilder for ItufuruArchiveBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn default_archive_encoding(&self) -> Option { + Some(Encoding::Cp932) + } + + fn build_script( + &self, + data: Vec, + _filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(ItufuruArchive::new( + MemReader::new(data), + archive_encoding, + config, + )?)) + } + + fn build_script_from_file( + &self, + filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + if filename == "-" { + let data = crate::utils::files::read_file(filename)?; + Ok(Box::new(ItufuruArchive::new( + MemReader::new(data), + archive_encoding, + config, + )?)) + } else { + let f = std::fs::File::open(filename)?; + let reader = std::io::BufReader::new(f); + Ok(Box::new(ItufuruArchive::new( + reader, + archive_encoding, + config, + )?)) + } + } + + fn build_script_from_reader( + &self, + reader: Box, + _filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(ItufuruArchive::new( + reader, + archive_encoding, + config, + )?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["scd"] + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 && buf.starts_with(b"SCR\0") { + Some(1) + } else { + None + } + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::YaneuraoItufuruArc + } + + fn is_archive(&self) -> bool { + true + } + + fn create_archive( + &self, + filename: &str, + files: &[&str], + encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + let f = std::fs::File::create(filename)?; + let writer = std::io::BufWriter::new(f); + let archive = ItufuruArchiveWriter::new(writer, files, encoding, config)?; + Ok(Box::new(archive)) + } +} + +#[derive(Debug, StructPack, StructUnpack)] +struct ItufuruFileHeader { + #[fstring = 12] + file_name: String, + offset: u32, +} + +#[derive(Debug, StructPack)] +struct CustomHeader { + #[fstring = 12] + file_name: String, + offset: u32, + #[skip_pack] + size: u32, +} + +struct Entry { + name: String, + data: Vec, +} + +impl ArchiveContent for Entry { + fn name(&self) -> &str { + &self.name + } + + fn data(&self) -> &[u8] { + &self.data + } + + fn script_type(&self) -> Option<&ScriptType> { + Some(&ScriptType::YaneuraoItufuru) + } +} + +#[derive(Debug)] +pub struct ItufuruArchive { + reader: Crypto, + first_file_offset: u32, + files: Vec, +} + +impl ItufuruArchive { + pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result { + let mut header = [0u8; 4]; + reader.read_exact(&mut header)?; + if &header != b"SCR\0" { + return Err(anyhow::anyhow!("Invalid Itufuru archive header")); + } + let file_count = reader.read_u32()?; + let first_file_offset = reader.read_u32()?; + reader.read_u32()?; // Skip unused field + let mut reader = Crypto::new(reader, 0xA5); + let mut tfiles = Vec::with_capacity(file_count as usize); + for _ in 0..file_count { + let file = ItufuruFileHeader::unpack(&mut reader, false, archive_encoding)?; + tfiles.push(file); + } + let mut files = Vec::with_capacity(tfiles.len()); + if !tfiles.is_empty() { + for i in 0..tfiles.len() - 1 { + let file = CustomHeader { + file_name: tfiles[i].file_name.clone(), + offset: tfiles[i].offset, + size: tfiles[i + 1].offset - tfiles[i].offset, + }; + files.push(file); + } + let last_file = &tfiles[tfiles.len() - 1]; + let file = CustomHeader { + file_name: last_file.file_name.clone(), + offset: last_file.offset, + size: reader.seek(SeekFrom::End(0))? as u32 - last_file.offset - first_file_offset, + }; + files.push(file); + } + Ok(ItufuruArchive { + reader, + first_file_offset, + files, + }) + } +} + +impl Script for ItufuruArchive { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn is_archive(&self) -> bool { + true + } + + fn iter_archive<'a>(&'a mut self) -> Result> + 'a>> { + Ok(Box::new( + self.files.iter().map(|s| Ok(s.file_name.to_owned())), + )) + } + + fn iter_archive_mut<'a>( + &'a mut self, + ) -> Result>> + 'a>> { + Ok(Box::new(ItufuruArchiveIter { + entries: self.files.iter(), + reader: &mut self.reader, + first_file_offset: self.first_file_offset, + })) + } +} + +struct ItufuruArchiveIter<'a, T: Iterator, R: Read + Seek> { + entries: T, + reader: &'a mut R, + first_file_offset: u32, +} + +impl<'a, T: Iterator, R: Read + Seek> Iterator + for ItufuruArchiveIter<'a, T, R> +{ + type Item = Result>; + + fn next(&mut self) -> Option { + if let Some(entry) = self.entries.next() { + let file_offset = entry.offset as usize; + match self.reader.peek_extract_at_vec( + file_offset + self.first_file_offset as usize, + entry.size as usize, + ) { + Ok(data) => { + let name = entry.file_name.clone(); + Some(Ok(Box::new(Entry { name, data }))) + } + Err(e) => Some(Err(anyhow::anyhow!( + "Failed to read file {}: {}", + entry.file_name, + e + ))), + } + } else { + None + } + } +} + +pub struct ItufuruArchiveWriter { + writer: T, + headers: HashMap, + first_file_offset: u32, + encoding: Encoding, +} + +impl ItufuruArchiveWriter { + pub fn new( + mut writer: T, + files: &[&str], + encoding: Encoding, + _config: &ExtraConfig, + ) -> Result { + writer.write_all(b"SCR\0")?; + let file_count = files.len() as u32; + writer.write_u32(file_count)?; + let first_file_offset = 0x10 + file_count * 16; // 16 bytes per file header + writer.write_u32(first_file_offset)?; + writer.write_u32(0)?; // Unused field + let mut headers = HashMap::new(); + for file in files { + headers.insert( + file.to_string(), + CustomHeader { + file_name: file.to_string(), + offset: 0, + size: 0, + }, + ); + } + let mut crypto = Crypto::new(&mut writer, 0xA5); + for (_, header) in headers.iter() { + header.pack(&mut crypto, false, encoding)?; + } + Ok(ItufuruArchiveWriter { + writer, + headers, + first_file_offset, + encoding, + }) + } +} + +impl Archive for ItufuruArchiveWriter { + fn new_file<'a>(&'a mut self, name: &str) -> Result> { + let entry = self + .headers + .get_mut(name) + .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?; + if entry.size != 0 { + return Err(anyhow::anyhow!("File '{}' already exists in archive", name)); + } + entry.offset = self.writer.stream_position()? as u32 - self.first_file_offset; + Ok(Box::new(ItufuruArchiveWriterEntry::new( + &mut self.writer, + entry, + self.first_file_offset, + ))) + } + fn write_header(&mut self) -> Result<()> { + let mut crypto = Crypto::new(&mut self.writer, 0xA5); + let mut entries = self.headers.values().collect::>(); + entries.sort_by_key(|h| h.offset); + crypto.seek(SeekFrom::Start(16))?; + for entry in entries.iter() { + entry.pack(&mut crypto, false, self.encoding)?; + } + Ok(()) + } +} + +pub struct ItufuruArchiveWriterEntry<'a, T: Write + Seek> { + writer: Crypto<&'a mut T>, + header: &'a mut CustomHeader, + first_file_offset: u32, + pos: usize, +} + +impl<'a, T: Write + Seek> ItufuruArchiveWriterEntry<'a, T> { + fn new(writer: &'a mut T, header: &'a mut CustomHeader, first_file_offset: u32) -> Self { + let writer = Crypto::new(writer, 0xA5); + ItufuruArchiveWriterEntry { + writer, + header, + first_file_offset, + pos: 0, + } + } +} + +impl<'a, T: Write + Seek> Write for ItufuruArchiveWriterEntry<'a, T> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.writer.seek(SeekFrom::Start( + self.header.offset as u64 + self.first_file_offset as u64 + self.pos as u64, + ))?; + let written = self.writer.write(buf)?; + self.pos += written; + self.header.size = self.header.size.max(self.pos as u32); + Ok(written) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.writer.flush() + } +} + +impl<'a, T: Write + Seek> Seek for ItufuruArchiveWriterEntry<'a, T> { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + let new_pos = match pos { + SeekFrom::Start(offset) => offset as usize, + SeekFrom::End(offset) => { + if offset < 0 { + if (-offset) as usize > self.header.size as usize { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek from end exceeds file length", + )); + } + self.header.size as usize - (-offset) as usize + } else { + self.header.size as usize + offset as usize + } + } + SeekFrom::Current(offset) => { + if offset < 0 { + if (-offset) as usize > self.pos { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek from current exceeds current position", + )); + } + self.pos.saturating_sub((-offset) as usize) + } else { + self.pos + offset as usize + } + } + }; + self.pos = new_pos; + Ok(self.pos as u64) + } + + fn stream_position(&mut self) -> std::io::Result { + Ok(self.pos as u64) + } +} diff --git a/src/scripts/yaneurao/itufuru/crypto.rs b/src/scripts/yaneurao/itufuru/crypto.rs new file mode 100644 index 0000000..9f46bbd --- /dev/null +++ b/src/scripts/yaneurao/itufuru/crypto.rs @@ -0,0 +1,59 @@ +use std::io::{Read, Seek, Write}; + +pub struct Crypto { + reader: T, + key: u8, +} + +impl Crypto { + pub fn new(reader: T, key: u8) -> Self { + Crypto { reader, key } + } +} + +impl Read for Crypto { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let read_bytes = self.reader.read(buf)?; + for byte in &mut buf[..read_bytes] { + *byte ^= self.key; + } + Ok(read_bytes) + } +} + +impl Seek for Crypto { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.reader.seek(pos) + } + + fn rewind(&mut self) -> std::io::Result<()> { + self.reader.rewind() + } + + fn stream_position(&mut self) -> std::io::Result { + self.reader.stream_position() + } +} + +impl Write for Crypto { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let mut encrypted_buf = buf.to_vec(); + for byte in &mut encrypted_buf { + *byte ^= self.key; + } + self.reader.write(&encrypted_buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.reader.flush() + } +} + +impl std::fmt::Debug for Crypto { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Crypto") + .field("reader", &self.reader) + .field("key", &self.key) + .finish() + } +} diff --git a/src/scripts/yaneurao/itufuru/mod.rs b/src/scripts/yaneurao/itufuru/mod.rs new file mode 100644 index 0000000..4bc5865 --- /dev/null +++ b/src/scripts/yaneurao/itufuru/mod.rs @@ -0,0 +1,3 @@ +pub mod archive; +mod crypto; +pub mod script; diff --git a/src/scripts/yaneurao/itufuru/script.rs b/src/scripts/yaneurao/itufuru/script.rs new file mode 100644 index 0000000..16b26dd --- /dev/null +++ b/src/scripts/yaneurao/itufuru/script.rs @@ -0,0 +1,164 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::{decode_to_string, encode_string}; +use anyhow::Result; + +#[derive(Debug)] +pub struct ItufuruScriptBuilder {} + +impl ItufuruScriptBuilder { + pub const fn new() -> Self { + ItufuruScriptBuilder {} + } +} + +impl ScriptBuilder for ItufuruScriptBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + data: Vec, + _filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(ItufuruScript::new(data, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &[] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::YaneuraoItufuru + } +} + +#[derive(Debug)] +struct ItufuruString { + len_pos: usize, + len: u16, +} + +#[derive(Debug)] +pub struct ItufuruScript { + data: MemReader, + strings: Vec, + encoding: Encoding, +} + +impl ItufuruScript { + pub fn new(buf: Vec, encoding: Encoding, _config: &ExtraConfig) -> Result { + let mut reader = MemReader::new(buf); + let mut strings = Vec::new(); + let len = reader.data.len(); + + while reader.pos + 1 < len { + let instr = reader.read_u16()?; + // 普通文本 0x2 + // 选项 0x1e + // 文件名 0x1 + // 背景 0x13 + // 声音 0x27 + if instr == 0x2 || instr == 0x1e || instr == 0x1 || instr == 0x13 || instr == 0x27 { + let len_pos = reader.pos; + let len = reader.read_u16()?; + match reader.read_cstring() { + Ok(s) => { + let slen = s.as_bytes_with_nul().len() as u16; + if slen != len { + reader.pos = len_pos; + continue; + } + if instr == 0x2 && !s.as_bytes().ends_with(b"\n") { + reader.pos = len_pos; + continue; + } + if instr != 0x2 && instr != 0x1e { + continue; + } + strings.push(ItufuruString { len_pos, len }); + } + Err(_) => { + reader.pos = len_pos; + continue; + } + } + } + } + + Ok(ItufuruScript { + data: reader, + strings, + encoding, + }) + } +} + +impl Script for ItufuruScript { + 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(); + for i in self.strings.iter() { + let str_pos = i.len_pos + 2; // Skip the length bytes + let s = self.data.cpeek_cstring_at(str_pos)?; + let decoded = decode_to_string(self.encoding, s.as_bytes())?; + messages.push(Message { + name: None, + message: decoded, + }); + } + Ok(messages) + } + + fn import_messages<'a>( + &'a self, + messages: Vec, + mut file: Box, + encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + if self.strings.len() != messages.len() { + return Err(anyhow::anyhow!( + "Number of messages does not match the number of strings in the script" + )); + } + let mut old_pos = 0; + for (old, new) in self.strings.iter().zip(messages) { + if old_pos < old.len_pos { + file.write_all(&self.data.data[old_pos..old.len_pos])?; + old_pos = old.len_pos; + } + let mut nstr = new.message; + if let Some(repl) = replacement { + for (from, to) in repl.map.iter() { + nstr = nstr.replace(from, to); + } + } + if !nstr.ends_with('\n') { + nstr.push('\n'); + } + let encoded = encode_string(encoding, &nstr, false)?; + let new_len = encoded.len() as u16 + 1; + file.write_u16(new_len)?; + file.write_all(&encoded)?; + file.write_all(&[0])?; // Null terminator + old_pos += 2 + old.len as usize; + } + if old_pos < self.data.data.len() { + file.write_all(&self.data.data[old_pos..])?; + } + Ok(()) + } +} diff --git a/src/scripts/yaneurao/mod.rs b/src/scripts/yaneurao/mod.rs new file mode 100644 index 0000000..d5bc7ee --- /dev/null +++ b/src/scripts/yaneurao/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "yaneurao-itufuru")] +pub mod itufuru; diff --git a/src/types.rs b/src/types.rs index 1053f3b..ebf6fca 100644 --- a/src/types.rs +++ b/src/types.rs @@ -226,6 +226,14 @@ pub enum ScriptType { #[cfg(feature = "escude")] /// Escude list script EscudeList, + #[cfg(feature = "yaneurao-itufuru")] + #[value(alias("itufuru"))] + /// Yaneurao Itufuru script + YaneuraoItufuru, + #[cfg(feature = "yaneurao-itufuru")] + #[value(alias("itufuru-arc"))] + /// Yaneurao Itufuru script archive + YaneuraoItufuruArc, } #[derive(Debug, Serialize, Deserialize)]