From 644cdefe7074321a9f7dd2a770ec6216b830465d Mon Sep 17 00:00:00 2001 From: lifegpc Date: Wed, 30 Jul 2025 23:06:31 +0800 Subject: [PATCH] Add circus pck support --- Cargo.toml | 3 +- src/scripts/circus/archive/mod.rs | 1 + src/scripts/circus/archive/pck.rs | 443 ++++++++++++++++++++++++++++++ src/scripts/circus/mod.rs | 2 + src/scripts/mod.rs | 2 + src/types.rs | 3 + 6 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 src/scripts/circus/archive/mod.rs create mode 100644 src/scripts/circus/archive/pck.rs diff --git a/Cargo.toml b/Cargo.toml index a041e24..33881ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ utf16string = "0.2" zstd = { version = "0.13", optional = true } [features] -default = ["artemis", "artemis-arc", "bgi", "bgi-arc", "bgi-img", "cat-system", "cat-system-arc", "cat-system-img", "circus", "circus-img", "escude", "escude-arc", "hexen-haus", "kirikiri", "kirikiri-img", "will-plus", "yaneurao", "yaneurao-itufuru"] +default = ["artemis", "artemis-arc", "bgi", "bgi-arc", "bgi-img", "cat-system", "cat-system-arc", "cat-system-img", "circus", "circus-arc", "circus-img", "escude", "escude-arc", "hexen-haus", "kirikiri", "kirikiri-img", "will-plus", "yaneurao", "yaneurao-itufuru"] artemis = ["utils-escape"] artemis-arc = ["artemis", "msg_tool_macro/artemis-arc", "sha1"] bgi = [] @@ -41,6 +41,7 @@ cat-system = ["fancy-regex", "flate2", "int-enum"] cat-system-arc = ["cat-system", "blowfish", "utils-crc32"] cat-system-img = ["cat-system", "flate2", "image", "utils-bit-stream"] circus = [] +circus-arc = ["circus"] circus-img = ["circus", "image", "flate2", "zstd"] escude = ["int-enum"] escude-arc = ["escude", "rand", "utils-bit-stream"] diff --git a/src/scripts/circus/archive/mod.rs b/src/scripts/circus/archive/mod.rs new file mode 100644 index 0000000..788b293 --- /dev/null +++ b/src/scripts/circus/archive/mod.rs @@ -0,0 +1 @@ +pub mod pck; diff --git a/src/scripts/circus/archive/pck.rs b/src/scripts/circus/archive/pck.rs new file mode 100644 index 0000000..30877fd --- /dev/null +++ b/src/scripts/circus/archive/pck.rs @@ -0,0 +1,443 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use msg_tool_macro::*; +use std::collections::HashMap; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::sync::{Arc, Mutex}; + +#[derive(Debug)] +pub struct PckArchiveBuilder {} + +impl PckArchiveBuilder { + pub const fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for PckArchiveBuilder { + 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(PckArchive::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(PckArchive::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(PckArchive::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(PckArchive::new(reader, archive_encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["pck", "dat"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::CircusPck + } + + 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); + Ok(Box::new(PckArchiveWriter::new( + writer, files, encoding, config, + )?)) + } +} + +#[derive(Debug, Clone, StructPack, StructUnpack)] +struct PckFileHeader { + #[fstring = 0x38] + name: String, + offset: u32, + size: u32, +} + +struct Entry { + header: PckFileHeader, + reader: Arc>, + pos: usize, + script_type: Option, +} + +impl ArchiveContent for Entry { + fn name(&self) -> &str { + &self.header.name + } + + fn script_type(&self) -> Option<&ScriptType> { + self.script_type.as_ref() + } +} + +impl Read for Entry { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let mut reader = self.reader.lock().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to lock mutex: {}", e), + ) + })?; + reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?; + let bytes_read = buf.len().min(self.header.size as usize - self.pos); + if bytes_read == 0 { + return Ok(0); + } + let bytes_read = reader.read(&mut buf[..bytes_read])?; + self.pos += bytes_read; + Ok(bytes_read) + } +} + +impl Seek for Entry { + 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) + } +} + +#[derive(Debug)] +pub struct PckArchive { + reader: Arc>, + entries: Vec, +} + +impl PckArchive { + pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result { + let file_count = reader.read_u32()?; + // (offset, size) + let mut offset_list = Vec::with_capacity(file_count as usize); + for _ in 0..file_count { + let offset = reader.read_u32()?; + let size = reader.read_u32()?; + offset_list.push((offset, size)); + } + for i in 1..file_count as usize { + let (prev_offset, prev_size) = offset_list[i - 1]; + let offset = offset_list[i].0; + if prev_offset + prev_size > offset { + return Err(anyhow::anyhow!( + "PckArchive: Overlapping entries detected at index {}: previous entry ends at {}, current entry starts at {}", + i - 1, + prev_offset + prev_size, + offset + )); + } + } + let mut entries = Vec::with_capacity(file_count as usize); + for (i, (offset, size)) in offset_list.into_iter().enumerate() { + let header: PckFileHeader = reader.read_struct(false, archive_encoding)?; + if header.offset != offset { + return Err(anyhow::anyhow!( + "PckArchive: Header offset mismatch at entry {}: expected {}, got {}", + i, + offset, + header.offset + )); + } + if header.size != size { + return Err(anyhow::anyhow!( + "PckArchive: Header size mismatch at entry {}: expected {}, got {}", + i, + size, + header.size + )); + } + entries.push(header); + } + Ok(Self { + reader: Arc::new(Mutex::new(reader)), + entries, + }) + } +} + +impl Script for PckArchive { + 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.entries.iter().map(|e| Ok(e.name.clone())))) + } + + fn iter_archive_mut<'a>( + &'a mut self, + ) -> Result>> + 'a>> { + Ok(Box::new(PckArchiveIter { + entries: self.entries.iter(), + reader: self.reader.clone(), + })) + } +} + +fn detect_script_type(_buf: &[u8], _buf_len: usize, _filename: &str) -> Option { + #[cfg(feature = "circus-img")] + if _buf_len >= 4 && _buf.starts_with(b"CRXG") { + return Some(ScriptType::CircusCrx); + } + None +} + +struct PckArchiveIter<'a, T: Iterator, R: Read + Seek> { + entries: T, + reader: Arc>, +} + +impl<'a, T: Iterator, R: Read + Seek + 'static> Iterator + for PckArchiveIter<'a, T, R> +{ + type Item = Result>; + + fn next(&mut self) -> Option { + let entry = match self.entries.next() { + Some(e) => e, + None => return None, + }; + let mut entry = Entry { + header: entry.clone(), + reader: self.reader.clone(), + pos: 0, + script_type: None, + }; + let mut buf = [0; 32]; + let readed = match entry.read(&mut buf) { + Ok(readed) => readed, + Err(e) => { + return Some(Err(anyhow::anyhow!( + "Failed to read entry '{}': {}", + entry.header.name, + e + ))); + } + }; + entry.pos = 0; + entry.script_type = detect_script_type(&buf, readed, &entry.header.name); + Some(Ok(Box::new(entry))) + } +} + +pub struct PckArchiveWriter { + writer: T, + headers: HashMap, + encoding: Encoding, +} + +impl PckArchiveWriter { + pub fn new( + mut writer: T, + files: &[&str], + encoding: Encoding, + _config: &ExtraConfig, + ) -> Result { + let file_count = files.len() as u32; + writer.write_u32(file_count)?; + let mut headers = HashMap::new(); + for _ in 0..file_count { + writer.write_u32(0)?; // Placeholder for offset + writer.write_u32(0)?; // Placeholder for size + } + for file in files { + let header = PckFileHeader { + name: file.to_string(), + offset: 0, + size: 0, + }; + header.pack(&mut writer, false, encoding)?; + headers.insert(file.to_string(), header); + } + Ok(PckArchiveWriter { + writer, + headers, + encoding, + }) + } +} + +impl Archive for PckArchiveWriter { + 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.offset != 0 || entry.size != 0 { + return Err(anyhow::anyhow!("File '{}' already exists in archive", name)); + } + self.writer.seek(SeekFrom::End(0))?; + entry.offset = self.writer.stream_position()? as u32; + let file = PckArchiveFile { + header: entry, + writer: &mut self.writer, + pos: 0, + }; + Ok(Box::new(file)) + } + + fn write_header(&mut self) -> Result<()> { + self.writer.seek(SeekFrom::Start(0x4))?; + let mut files = self.headers.iter().map(|(_, d)| d).collect::>(); + files.sort_by_key(|f| f.offset); + for file in files.iter() { + self.writer.write_u32(file.offset)?; + self.writer.write_u32(file.size)?; + } + for file in files { + file.pack(&mut self.writer, false, self.encoding)?; + } + Ok(()) + } +} + +pub struct PckArchiveFile<'a, T: Write + Seek> { + header: &'a mut PckFileHeader, + writer: &'a mut T, + pos: usize, +} + +impl<'a, T: Write + Seek> Write for PckArchiveFile<'a, T> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.writer + .seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?; + let bytes_written = self.writer.write(buf)?; + self.pos += bytes_written; + self.header.size = self.header.size.max(self.pos as u32); + Ok(bytes_written) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.writer.flush() + } +} + +impl<'a, T: Write + Seek> Seek for PckArchiveFile<'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) + } +} diff --git a/src/scripts/circus/mod.rs b/src/scripts/circus/mod.rs index a74b4cb..0ea7903 100644 --- a/src/scripts/circus/mod.rs +++ b/src/scripts/circus/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "circus-arc")] +pub mod archive; #[cfg(feature = "circus-img")] pub mod image; mod info; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 6336743..ed02228 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -84,6 +84,8 @@ lazy_static::lazy_static! { Box::new(circus::image::crx::CrxImageBuilder::new()), #[cfg(feature = "cat-system")] Box::new(cat_system::cstl::CstlScriptBuilder::new()), + #[cfg(feature = "circus-arc")] + Box::new(circus::archive::pck::PckArchiveBuilder::new()), ]; pub static ref ALL_EXTS: Vec = BUILDER.iter().flat_map(|b| b.extensions()).map(|s| s.to_string()).collect(); diff --git a/src/types.rs b/src/types.rs index 6d61b33..c745ea6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -309,6 +309,9 @@ pub enum ScriptType { #[cfg(feature = "circus")] /// Circus MES script Circus, + #[cfg(feature = "circus-arc")] + /// Circus PCK archive + CircusPck, #[cfg(feature = "circus-img")] /// Circus CRX Image CircusCrx,