mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-06 12:58:45 +08:00
Add support for ExHibit GRP Archive File (.grp)
This commit is contained in:
@@ -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", "softpal-arc"]
|
||||
all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "softpal-arc"]
|
||||
all-audio = ["bgi-audio", "circus-audio"]
|
||||
artemis = ["stylua", "utils-escape"]
|
||||
artemis-panmimisoft = ["artemis", "rust-ini"]
|
||||
@@ -75,6 +75,7 @@ entis-gls = ["xml5ever", "markup5ever", "markup5ever_rcdom"]
|
||||
escude = ["int-enum"]
|
||||
escude-arc = ["escude", "rand", "utils-bit-stream"]
|
||||
ex-hibit = []
|
||||
ex-hibit-arc = ["ex-hibit"]
|
||||
favorite = []
|
||||
hexen-haus = ["memchr", "utils-str"]
|
||||
kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "lz4", "utils-escape"]
|
||||
|
||||
@@ -153,6 +153,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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| `ex-hibit` | `ex-hibit` | ExHibit Script File (.rld) | ✔️ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | |
|
||||
|
||||
| Archive Type | Feature Name | Name | Unpack | Pack | Remarks |
|
||||
|---|---|---|---|---|---|
|
||||
| `ex-hibit-grp` | `ex-hibit-arc` | ExHibit GRP Archive File (.grp) | ✔️ | ❌ | |
|
||||
### Favorite
|
||||
| Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
|
||||
@@ -1263,7 +1263,9 @@ impl Script for CrxImage {
|
||||
pixel_size,
|
||||
&row_type,
|
||||
)?
|
||||
} else if self.row_type.is_best() || (self.row_type.is_origin() && !self.data.is_row_encoded()){
|
||||
} else if self.row_type.is_best()
|
||||
|| (self.row_type.is_origin() && !self.data.is_row_encoded())
|
||||
{
|
||||
Self::encode_image_best(&data.data, new_header.width, new_header.height, pixel_size)?
|
||||
} else if let CircusCrxMode::Fixed(mode) = self.row_type {
|
||||
Self::encode_image_fixed(
|
||||
|
||||
515
src/scripts/ex_hibit/arc/grp.rs
Normal file
515
src/scripts/ex_hibit/arc/grp.rs
Normal file
@@ -0,0 +1,515 @@
|
||||
//! ExHibit GRP archive extractor.
|
||||
use crate::ext::io::*;
|
||||
use crate::scripts::base::*;
|
||||
use crate::types::*;
|
||||
use anyhow::{Context, Result};
|
||||
use std::fmt::Debug;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Builder for ExHibit GRP archives.
|
||||
pub struct ExHibitGrpArchiveBuilder {}
|
||||
|
||||
impl ExHibitGrpArchiveBuilder {
|
||||
/// Creates a new builder instance.
|
||||
pub const fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn build_with_reader<T>(
|
||||
&self,
|
||||
reader: T,
|
||||
filename: &str,
|
||||
archive_encoding: Encoding,
|
||||
config: &ExtraConfig,
|
||||
) -> Result<Box<dyn Script>>
|
||||
where
|
||||
T: Read + Seek + Debug + 'static,
|
||||
{
|
||||
Ok(Box::new(ExHibitGrpArchive::new(
|
||||
reader,
|
||||
filename,
|
||||
archive_encoding,
|
||||
config,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ScriptBuilder for ExHibitGrpArchiveBuilder {
|
||||
fn default_encoding(&self) -> Encoding {
|
||||
Encoding::Cp932
|
||||
}
|
||||
|
||||
fn default_archive_encoding(&self) -> Option<Encoding> {
|
||||
Some(Encoding::Cp932)
|
||||
}
|
||||
|
||||
fn build_script(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
filename: &str,
|
||||
_encoding: Encoding,
|
||||
archive_encoding: Encoding,
|
||||
config: &ExtraConfig,
|
||||
_archive: Option<&Box<dyn Script>>,
|
||||
) -> Result<Box<dyn Script>> {
|
||||
self.build_with_reader(MemReader::new(data), filename, 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 == "-" {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Reading ExHibit GRP from stdin is not supported; provide a file path."
|
||||
));
|
||||
}
|
||||
let file = std::fs::File::open(filename)
|
||||
.with_context(|| format!("Failed to open '{}'.", filename))?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
self.build_with_reader(reader, filename, 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>> {
|
||||
self.build_with_reader(reader, filename, archive_encoding, config)
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["grp"]
|
||||
}
|
||||
|
||||
fn script_type(&self) -> &'static ScriptType {
|
||||
&ScriptType::ExHibitGrp
|
||||
}
|
||||
|
||||
fn is_this_format(&self, filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
|
||||
if !matches_grp_name(filename) {
|
||||
return None;
|
||||
}
|
||||
if buf_len >= 4 && buf.starts_with(b"AiFS") {
|
||||
return None;
|
||||
}
|
||||
Some(10)
|
||||
}
|
||||
|
||||
fn is_archive(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct GrpFileEntry {
|
||||
name: String,
|
||||
offset: u64,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// ExHibit GRP archive instance.
|
||||
pub struct ExHibitGrpArchive<T: Read + Seek + Debug> {
|
||||
reader: Arc<Mutex<T>>,
|
||||
entries: Vec<GrpFileEntry>,
|
||||
}
|
||||
|
||||
impl<T: Read + Seek + Debug> ExHibitGrpArchive<T> {
|
||||
fn new(
|
||||
mut reader: T,
|
||||
filename: &str,
|
||||
_archive_encoding: Encoding,
|
||||
_config: &ExtraConfig,
|
||||
) -> Result<Self> {
|
||||
let mut header = [0u8; 4];
|
||||
reader
|
||||
.peek_exact_at(0, &mut header)
|
||||
.context("Failed to read GRP header.")?;
|
||||
if &header == b"AiFS" {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Input file is a TOC (AiFS) rather than an archive."
|
||||
));
|
||||
}
|
||||
|
||||
let path = Path::new(filename);
|
||||
let (toc_path, arc_index) = locate_toc_file(path).context("Failed to locate TOC file.")?;
|
||||
|
||||
let archive_size = (&mut reader)
|
||||
.stream_length()
|
||||
.context("Failed to determine archive size.")?;
|
||||
|
||||
let entries = parse_toc_entries(&toc_path, arc_index, archive_size)
|
||||
.with_context(|| format!("Failed to parse TOC '{}'.", toc_path.display()))?;
|
||||
|
||||
Ok(Self {
|
||||
reader: Arc::new(Mutex::new(reader)),
|
||||
entries,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read + Seek + Debug + 'static> Script for ExHibitGrpArchive<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: {} (max: {}).",
|
||||
index,
|
||||
self.entries.len()
|
||||
));
|
||||
}
|
||||
let entry = self.entries[index].clone();
|
||||
Ok(Box::new(GrpEntry::new(entry, self.reader.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
struct GrpEntry<T: Read + Seek> {
|
||||
info: GrpFileEntry,
|
||||
reader: Arc<Mutex<T>>,
|
||||
pos: u64,
|
||||
}
|
||||
|
||||
impl<T: Read + Seek> GrpEntry<T> {
|
||||
fn new(info: GrpFileEntry, reader: Arc<Mutex<T>>) -> Self {
|
||||
Self {
|
||||
info,
|
||||
reader,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn remaining(&self) -> u64 {
|
||||
self.info.size.saturating_sub(self.pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read + Seek> ArchiveContent for GrpEntry<T> {
|
||||
fn name(&self) -> &str {
|
||||
&self.info.name
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read + Seek> Read for GrpEntry<T> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
if buf.is_empty() || self.pos >= self.info.size {
|
||||
return Ok(0);
|
||||
}
|
||||
let remaining = self.remaining() as usize;
|
||||
if remaining == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let to_read = buf.len().min(remaining);
|
||||
let mut reader = self.reader.lock().map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to lock reader mutex: {}", e),
|
||||
)
|
||||
})?;
|
||||
reader.seek(SeekFrom::Start(self.info.offset + self.pos))?;
|
||||
let bytes = reader.read(&mut buf[..to_read])?;
|
||||
self.pos = self.pos.checked_add(bytes as u64).ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, "Read position overflow.")
|
||||
})?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read + Seek> Seek for GrpEntry<T> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
|
||||
let new_pos = match pos {
|
||||
SeekFrom::Start(offset) => offset,
|
||||
SeekFrom::End(offset) => {
|
||||
let signed = self.info.size as i128 + offset as i128;
|
||||
if signed < 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Seek before entry start is not allowed.",
|
||||
));
|
||||
}
|
||||
signed as u64
|
||||
}
|
||||
SeekFrom::Current(offset) => {
|
||||
let signed = self.pos as i128 + offset as i128;
|
||||
if signed < 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Seek before entry start is not allowed.",
|
||||
));
|
||||
}
|
||||
signed as u64
|
||||
}
|
||||
};
|
||||
if new_pos > self.info.size {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Seek beyond entry size is not allowed.",
|
||||
));
|
||||
}
|
||||
self.pos = new_pos;
|
||||
Ok(self.pos)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NameInfo {
|
||||
digits_offset: usize,
|
||||
digits_len: usize,
|
||||
arc_num: u32,
|
||||
}
|
||||
|
||||
fn matches_grp_name(filename: &str) -> bool {
|
||||
Path::new(filename)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.and_then(|name| parse_name_info(name).ok())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn parse_name_info(name: &str) -> Result<NameInfo> {
|
||||
if name.len() < 7 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Filename '{}' is too short for GRP pattern.",
|
||||
name
|
||||
));
|
||||
}
|
||||
let prefix = &name[..3];
|
||||
if !prefix.eq_ignore_ascii_case("res") {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Filename '{}' does not start with 'res'.",
|
||||
name
|
||||
));
|
||||
}
|
||||
let suffix = &name[name.len() - 4..];
|
||||
if !suffix.eq_ignore_ascii_case(".grp") {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Filename '{}' does not end with '.grp'.",
|
||||
name
|
||||
));
|
||||
}
|
||||
let digits = &name[3..name.len() - 4];
|
||||
if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Filename '{}' does not contain a numeric sequence.",
|
||||
name
|
||||
));
|
||||
}
|
||||
let arc_num = digits.parse::<u32>().with_context(|| {
|
||||
format!(
|
||||
"Failed to parse archive number from '{}' (digits '{}').",
|
||||
name, digits
|
||||
)
|
||||
})?;
|
||||
Ok(NameInfo {
|
||||
digits_offset: 3,
|
||||
digits_len: digits.len(),
|
||||
arc_num,
|
||||
})
|
||||
}
|
||||
|
||||
fn locate_toc_file(path: &Path) -> Result<(PathBuf, u32)> {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Filename contains invalid UTF-8."))?;
|
||||
let info = parse_name_info(file_name)?;
|
||||
if info.arc_num == 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Archive '{}' has number 0 and therefore no preceding TOC file.",
|
||||
file_name
|
||||
));
|
||||
}
|
||||
|
||||
let mut toc_num = info.arc_num as i64 - 1;
|
||||
let mut arc_index: u32 = 1;
|
||||
while toc_num >= 0 {
|
||||
let digits = format!("{:0width$}", toc_num, width = info.digits_len);
|
||||
let mut candidate = String::with_capacity(file_name.len());
|
||||
candidate.push_str(&file_name[..info.digits_offset]);
|
||||
candidate.push_str(&digits);
|
||||
candidate.push_str(&file_name[info.digits_offset + info.digits_len..]);
|
||||
let candidate_path = path.with_file_name(&candidate);
|
||||
if !candidate_path.exists() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"TOC file '{}' does not exist.",
|
||||
candidate_path.display()
|
||||
));
|
||||
}
|
||||
let mut file = std::fs::File::open(&candidate_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to open TOC candidate '{}'.",
|
||||
candidate_path.display()
|
||||
)
|
||||
})?;
|
||||
let mut header = [0u8; 4];
|
||||
file.read_exact(&mut header).with_context(|| {
|
||||
format!("Failed to read header from '{}'.", candidate_path.display())
|
||||
})?;
|
||||
if &header == b"AiFS" {
|
||||
return Ok((candidate_path, arc_index));
|
||||
}
|
||||
toc_num -= 1;
|
||||
arc_index = arc_index
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| anyhow::anyhow!("Archive index overflow while searching TOC."))?;
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"Unable to locate a TOC (AiFS) file for '{}'.",
|
||||
file_name
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_toc_entries(
|
||||
toc_path: &Path,
|
||||
arc_index: u32,
|
||||
archive_size: u64,
|
||||
) -> Result<Vec<GrpFileEntry>> {
|
||||
let file = std::fs::File::open(toc_path)?;
|
||||
let mut reader = std::io::BufReader::new(file);
|
||||
let toc_len = reader.stream_length()?;
|
||||
if toc_len < 0x10 {
|
||||
return Err(anyhow::anyhow!("TOC file is too small."));
|
||||
}
|
||||
|
||||
reader.seek(SeekFrom::Start(0xC))?;
|
||||
let res_count = reader.read_i32()?;
|
||||
if res_count <= 0 {
|
||||
return Err(anyhow::anyhow!("TOC resource count is invalid."));
|
||||
}
|
||||
if arc_index as i64 > res_count as i64 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Archive index {} is out of range (resource count {}).",
|
||||
arc_index,
|
||||
res_count
|
||||
));
|
||||
}
|
||||
|
||||
let mut index_offset = 0x10u64;
|
||||
let mut arc_offset = None;
|
||||
for _ in 0..res_count {
|
||||
if index_offset + 0x10 > toc_len {
|
||||
break;
|
||||
}
|
||||
reader.seek(SeekFrom::Start(index_offset))?;
|
||||
let mut num = reader.read_i32()?;
|
||||
if num == 0x0100_0000 {
|
||||
index_offset = index_offset
|
||||
.checked_add(4)
|
||||
.ok_or_else(|| anyhow::anyhow!("Index offset overflow."))?;
|
||||
if index_offset + 4 > toc_len {
|
||||
break;
|
||||
}
|
||||
reader.seek(SeekFrom::Start(index_offset))?;
|
||||
num = reader.read_i32()?;
|
||||
}
|
||||
reader.seek(SeekFrom::Start(index_offset + 0xC))?;
|
||||
let entry_count = reader.read_u32()?;
|
||||
if num == arc_index as i32 {
|
||||
arc_offset = Some(index_offset);
|
||||
break;
|
||||
}
|
||||
let step = (entry_count as u64)
|
||||
.checked_mul(8)
|
||||
.and_then(|v| v.checked_add(0x10))
|
||||
.ok_or_else(|| anyhow::anyhow!("Index offset overflow while skipping entries."))?;
|
||||
index_offset = index_offset
|
||||
.checked_add(step)
|
||||
.ok_or_else(|| anyhow::anyhow!("Index offset overflow while iterating."))?;
|
||||
}
|
||||
|
||||
let arc_offset =
|
||||
arc_offset.ok_or_else(|| anyhow::anyhow!("Archive reference not found in TOC."))?;
|
||||
|
||||
reader.seek(SeekFrom::Start(arc_offset + 4))?;
|
||||
let start_index = reader.read_i32()?;
|
||||
if start_index < 0 {
|
||||
return Err(anyhow::anyhow!("Start index is negative."));
|
||||
}
|
||||
reader.seek(SeekFrom::Start(arc_offset + 0xC))?;
|
||||
let entry_count = reader.read_i32()?;
|
||||
if entry_count < 0 {
|
||||
return Err(anyhow::anyhow!("Entry count is negative."));
|
||||
}
|
||||
let entry_count = entry_count as u32;
|
||||
|
||||
let data_offset = arc_offset
|
||||
.checked_add(0x10)
|
||||
.ok_or_else(|| anyhow::anyhow!("Entry table offset overflow."))?;
|
||||
let table_len = (entry_count as u64)
|
||||
.checked_mul(8)
|
||||
.ok_or_else(|| anyhow::anyhow!("Entry table size overflow."))?;
|
||||
if data_offset + table_len > toc_len {
|
||||
return Err(anyhow::anyhow!("TOC entry table exceeds file size."));
|
||||
}
|
||||
|
||||
let mut entries = Vec::with_capacity(entry_count as usize);
|
||||
let mut entry_offset = data_offset;
|
||||
for i in 0..entry_count {
|
||||
reader.seek(SeekFrom::Start(entry_offset))?;
|
||||
let offset = reader.read_u32()? as u64;
|
||||
let size = reader.read_u32()? as u64;
|
||||
if size != 0 {
|
||||
let end = offset
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| anyhow::anyhow!("Entry size overflow."))?;
|
||||
if end > archive_size {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Entry {} exceeds archive size (offset {}, size {}).",
|
||||
i,
|
||||
offset,
|
||||
size
|
||||
));
|
||||
}
|
||||
let index = (start_index as u32)
|
||||
.checked_add(i)
|
||||
.ok_or_else(|| anyhow::anyhow!("Entry index overflow."))?;
|
||||
entries.push(GrpFileEntry {
|
||||
name: format!("{:05}.ogg", index),
|
||||
offset,
|
||||
size,
|
||||
});
|
||||
}
|
||||
entry_offset += 8;
|
||||
}
|
||||
|
||||
if entries.is_empty() {
|
||||
return Err(anyhow::anyhow!("Archive contains no entries."));
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
1
src/scripts/ex_hibit/arc/mod.rs
Normal file
1
src/scripts/ex_hibit/arc/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod grp;
|
||||
@@ -1,2 +1,4 @@
|
||||
//! ExHibit Scripts
|
||||
#[cfg(feature = "ex-hibit-arc")]
|
||||
pub mod arc;
|
||||
pub mod rld;
|
||||
|
||||
@@ -138,6 +138,8 @@ lazy_static::lazy_static! {
|
||||
Box::new(softpal::img::pgd::ge::PgdGeBuilder::new()),
|
||||
#[cfg(feature = "softpal-img")]
|
||||
Box::new(softpal::img::pgd::pgd3::Pgd3Builder::new()),
|
||||
#[cfg(feature = "ex-hibit-arc")]
|
||||
Box::new(ex_hibit::arc::grp::ExHibitGrpArchiveBuilder::new()),
|
||||
];
|
||||
/// A list of all script extensions.
|
||||
pub static ref ALL_EXTS: Vec<String> =
|
||||
|
||||
@@ -571,6 +571,9 @@ pub enum ScriptType {
|
||||
#[cfg(feature = "ex-hibit")]
|
||||
/// ExHibit rld script
|
||||
ExHibit,
|
||||
#[cfg(feature = "ex-hibit-arc")]
|
||||
/// ExHibit GRP archive
|
||||
ExHibitGrp,
|
||||
#[cfg(feature = "favorite")]
|
||||
/// Favorite hcb script
|
||||
Favorite,
|
||||
|
||||
Reference in New Issue
Block a user