Add HexenHaus ARCC archive (.arc) support

This commit is contained in:
2025-09-27 13:12:34 +08:00
parent 0fd2323ecc
commit 1cdf0bc7e8
7 changed files with 363 additions and 1 deletions

View File

@@ -54,7 +54,7 @@ default = ["all-fmt", "image-jpg", "image-jxl", "image-webp", "audio-flac", "jie
all-fmt = ["all-script", "all-img", "all-arc", "all-audio"]
all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "favorite", "hexen-haus", "kirikiri", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"]
all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "kirikiri-img", "softpal-img"]
all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "softpal-arc"]
all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "hexen-haus-arc", "softpal-arc"]
all-audio = ["bgi-audio", "circus-audio"]
artemis = ["stylua", "utils-escape"]
artemis-panmimisoft = ["artemis", "rust-ini"]
@@ -78,6 +78,7 @@ ex-hibit = []
ex-hibit-arc = ["ex-hibit"]
favorite = []
hexen-haus = ["memchr", "utils-str"]
hexen-haus-arc = ["hexen-haus"]
kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "lz4", "utils-escape"]
kirikiri-img = ["kirikiri", "image", "libtlg-rs"]
silky = []

View File

@@ -165,6 +165,10 @@ msg-tool create -t <script-type> <input> <output>
| Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks |
|---|---|---|---|---|---|---|---|---|---|---|
| `hexen-haus` | `hexen-haus` | HexenHaus Script File (.bin) | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | |
| Archive Type | Feature Name | Name | Unpack | Pack | Remarks |
|---|---|---|---|---|---|
| `hexen-haus-arcc` | `hexen-haus-arc` | HexenHaus Arcc Archive File (.arcc) | ✔️ | ❌ | |
### Kirikiri
| Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks |
|---|---|---|---|---|---|---|---|---|---|---|

View File

@@ -0,0 +1,340 @@
//! HexenHaus ARCC archive (.arc)
use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::encoding::decode_to_string;
use anyhow::Result;
use std::io::{Read, Seek, SeekFrom};
use std::sync::{Arc, Mutex};
#[derive(Debug)]
/// HexenHaus ARCC archive builder
pub struct HexenHausArccArchiveBuilder;
impl HexenHausArccArchiveBuilder {
/// Creates a new `HexenHausArccArchiveBuilder`
pub const fn new() -> Self {
HexenHausArccArchiveBuilder
}
}
impl ScriptBuilder for HexenHausArccArchiveBuilder {
fn default_encoding(&self) -> Encoding {
Encoding::Cp932
}
fn default_archive_encoding(&self) -> Option<Encoding> {
Some(Encoding::Cp932)
}
fn build_script(
&self,
buf: Vec<u8>,
_filename: &str,
_encoding: Encoding,
archive_encoding: Encoding,
config: &ExtraConfig,
_archive: Option<&Box<dyn Script>>,
) -> Result<Box<dyn Script>> {
Ok(Box::new(HexenHausArccArchive::new(
MemReader::new(buf),
archive_encoding,
config,
)?))
}
fn build_script_from_file(
&self,
filename: &str,
_encoding: Encoding,
archive_encoding: Encoding,
config: &ExtraConfig,
_archive: Option<&Box<dyn Script>>,
) -> Result<Box<dyn Script>> {
if filename == "-" {
let data = crate::utils::files::read_file(filename)?;
return Ok(Box::new(HexenHausArccArchive::new(
MemReader::new(data),
archive_encoding,
config,
)?));
}
let file = std::fs::File::open(filename)?;
let reader = std::io::BufReader::new(file);
Ok(Box::new(HexenHausArccArchive::new(
reader,
archive_encoding,
config,
)?))
}
fn build_script_from_reader(
&self,
reader: Box<dyn ReadSeek>,
_filename: &str,
_encoding: Encoding,
archive_encoding: Encoding,
config: &ExtraConfig,
_archive: Option<&Box<dyn Script>>,
) -> Result<Box<dyn Script>> {
Ok(Box::new(HexenHausArccArchive::new(
reader,
archive_encoding,
config,
)?))
}
fn extensions(&self) -> &'static [&'static str] {
&["arc"]
}
fn script_type(&self) -> &'static ScriptType {
&ScriptType::HexenHausArcc
}
fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
if buf_len >= 4 && buf.starts_with(b"ARCC") {
Some(10)
} else {
None
}
}
fn is_archive(&self) -> bool {
true
}
}
#[derive(Debug, Clone)]
struct HexenHausArccEntry {
name: String,
offset: u64,
size: u32,
}
#[derive(Debug)]
/// HexenHaus ARCC archive
pub struct HexenHausArccArchive<T: Read + Seek + std::fmt::Debug> {
reader: Arc<Mutex<T>>,
entries: Vec<HexenHausArccEntry>,
}
impl<T: Read + Seek + std::fmt::Debug> HexenHausArccArchive<T> {
/// Creates a new `HexenHausArccArchive`
pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
reader.seek(SeekFrom::Start(0))?;
let mut signature = [0u8; 4];
reader.read_exact(&mut signature)?;
if signature != *b"ARCC" {
return Err(anyhow::anyhow!("Invalid HexenHaus ARCC signature"));
}
reader.seek(SeekFrom::Start(0))?;
let reader = Arc::new(Mutex::new(reader));
let file_count = reader.cpeek_u32_at(0x14)?;
let entry_count = file_count as usize;
let mut index_offset = 0x2a_u64;
let mut tag = [0u8; 4];
reader.cpeek_exact_at(index_offset, &mut tag)?;
if &tag != b"NAME" {
return Err(anyhow::anyhow!("Missing NAME section in ARCC archive"));
}
let addr_offset = reader.cpeek_u64_at(index_offset + 4)?;
index_offset += 0x0e;
reader.cpeek_exact_at(index_offset, &mut tag)?;
if &tag != b"NIDX" {
return Err(anyhow::anyhow!("Missing NIDX section in ARCC archive"));
}
index_offset += 4;
for _ in 0..entry_count {
let _ = reader.cpeek_u32_at(index_offset + 2)?;
index_offset += 8;
}
reader.cpeek_exact_at(index_offset, &mut tag)?;
if &tag != b"EIDX" {
return Err(anyhow::anyhow!("Missing EIDX section in ARCC archive"));
}
index_offset += 4 + 8 * file_count as u64;
reader.cpeek_exact_at(index_offset, &mut tag)?;
if &tag != b"CINF" {
return Err(anyhow::anyhow!("Missing CINF section in ARCC archive"));
}
index_offset += 4;
let mut entries = Vec::with_capacity(entry_count);
for _ in 0..entry_count {
index_offset += 6;
let name_len = reader.cpeek_u16_at(index_offset)? as usize;
let mut name_buf = vec![0u8; name_len];
if name_len > 0 {
reader.cpeek_exact_at(index_offset + 4, &mut name_buf)?;
decrypt_name(&mut name_buf);
}
index_offset += 6 + name_len as u64;
let name = decode_to_string(archive_encoding, &name_buf, true)?;
entries.push(HexenHausArccEntry {
name,
offset: 0,
size: 0,
});
}
let mut addr_offset = addr_offset;
reader.cpeek_exact_at(addr_offset, &mut tag)?;
if &tag != b"ADDR" {
return Err(anyhow::anyhow!("Missing ADDR section in ARCC archive"));
}
addr_offset += 4;
for entry in &mut entries {
entry.offset = reader.cpeek_u64_at(addr_offset + 2)?;
addr_offset += 12;
}
for entry in &mut entries {
if reader.cpeek_and_equal_at(entry.offset, b"FILE").is_err() {
continue;
}
entry.size = reader.cpeek_u32_at(entry.offset + 0x18)?;
entry.offset += 0x22;
}
entries.retain(|entry| entry.size > 0);
if entries.is_empty() {
return Err(anyhow::anyhow!("ARCC archive contains no files"));
}
Ok(HexenHausArccArchive { reader, entries })
}
}
impl<T: Read + Seek + std::fmt::Debug + std::any::Any> Script for HexenHausArccArchive<T> {
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_filename<'a>(
&'a self,
) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
Ok(Box::new(
self.entries.iter().map(|entry| Ok(entry.name.clone())),
))
}
fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
}
fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
if index >= self.entries.len() {
return Err(anyhow::anyhow!(
"Index out of bounds: {} (total files: {})",
index,
self.entries.len()
));
}
let entry = &self.entries[index];
let header = self
.reader
.cpeek_at_vec(entry.offset, (entry.size as usize).min(16))?;
Ok(Box::new(Entry {
reader: self.reader.clone(),
header: entry.clone(),
pos: 0,
typ: super::detect_script_type(&entry.name, &header),
}))
}
}
struct Entry<T: Read + Seek> {
header: HexenHausArccEntry,
reader: Arc<Mutex<T>>,
pos: u64,
typ: Option<ScriptType>,
}
impl<T: Read + Seek> ArchiveContent for Entry<T> {
fn name(&self) -> &str {
&self.header.name
}
fn script_type(&self) -> Option<&ScriptType> {
self.typ.as_ref()
}
}
impl<T: Read + Seek> Read for Entry<T> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
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 + self.pos))?;
let bytes_read = buf.len().min(self.header.size as usize - self.pos as usize);
if bytes_read == 0 {
return Ok(0);
}
let bytes_read = reader.read(&mut buf[..bytes_read])?;
self.pos += bytes_read as u64;
Ok(bytes_read)
}
}
impl<T: Read + Seek> Seek for Entry<T> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let new_pos = match pos {
SeekFrom::Start(offset) => offset as u64,
SeekFrom::End(offset) => {
if offset < 0 {
if (-offset) as u64 > self.header.size as u64 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Seek from end exceeds file length",
));
}
self.header.size as u64 - (-offset) as u64
} else {
self.header.size as u64 + offset as u64
}
}
SeekFrom::Current(offset) => {
if offset < 0 {
if (-offset) as u64 > self.pos {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Seek from current exceeds current position",
));
}
self.pos.saturating_sub((-offset) as u64)
} else {
self.pos + offset as u64
}
}
};
self.pos = new_pos;
Ok(self.pos)
}
fn stream_position(&mut self) -> std::io::Result<u64> {
Ok(self.pos)
}
}
fn decrypt_name(buf: &mut [u8]) {
for byte in buf.iter_mut() {
*byte ^= 0x69;
}
}

View File

@@ -0,0 +1,10 @@
pub mod arcc;
use crate::types::ScriptType;
fn detect_script_type(_filename: &str, buf: &[u8]) -> Option<ScriptType> {
if buf.len() >= 4 && buf.starts_with(b"NORI") {
return Some(ScriptType::HexenHaus);
}
None
}

View File

@@ -1,2 +1,4 @@
//! HexenHaus Scripts
#[cfg(feature = "hexen-haus-arc")]
pub mod archive;
pub mod bin;

View File

@@ -140,6 +140,8 @@ lazy_static::lazy_static! {
Box::new(softpal::img::pgd::pgd3::Pgd3Builder::new()),
#[cfg(feature = "ex-hibit-arc")]
Box::new(ex_hibit::arc::grp::ExHibitGrpArchiveBuilder::new()),
#[cfg(feature = "hexen-haus-arc")]
Box::new(hexen_haus::archive::arcc::HexenHausArccArchiveBuilder::new()),
];
/// A list of all script extensions.
pub static ref ALL_EXTS: Vec<String> =

View File

@@ -595,6 +595,9 @@ pub enum ScriptType {
#[cfg(feature = "hexen-haus")]
/// HexenHaus bin script
HexenHaus,
#[cfg(feature = "hexen-haus-arc")]
/// HexenHaus Arcc archive
HexenHausArcc,
#[cfg(feature = "kirikiri")]
#[value(alias("kr-scn"))]
/// Kirikiri SCN script