Add three static PAK-format decoders in the Bevy-free formats crate and present them in the explorer: - t8ad.rs: linear 32bpp A8R8G8B8 2D texture (UI/HUD art). Dims at 0x14/0x18, header size keyed by the type field @0x1c (container-safe). Decodes the ~85% RGBA variants; defers the rest (likely DXT) rather than misdecoding. - ratc.rs: nested resource bundle — lists named children by magic scan. - lsta.rs: sprite list — walks the inline T8aD frames. Viewer: PakContent gains T8ad/Lsta/Ratc, decoded off-thread and bounded by an 8M-texel per-entry cap; detail views show the texture, a sprite grid, and a child list with thumbnails. Decoded images carry a "colours unverified" note — channel-order/sRGB stays on the dynamic-RE backlog. Tests: 12 new unit + 3 real-disc (T8aD >=70% decode over the hangar pack, LSTA frames, RATC named children incl. a decodable T8aD). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
132 lines
4.3 KiB
Rust
132 lines
4.3 KiB
Rust
//! `RATC` — a nested resource bundle.
|
|
//!
|
|
//! After a small header, a RATC holds named children — `foo.t32` (T8aD
|
|
//! textures), `foo.rat` (nested RATC), fonts, PNG — each immediately preceded by
|
|
//! its ASCII name string. The children are self-locating by their 4-char magic,
|
|
//! so we list them by scanning for those magics and pairing each with the name
|
|
//! run that precedes it. This is reliable for *listing* (names / types / sizes
|
|
//! are literal bytes); decoding a child's pixels is delegated to that child's
|
|
//! own parser ([`crate::t8ad`]).
|
|
|
|
/// A child resource inside a RATC bundle.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct RatcChild {
|
|
/// Name string preceding the child (e.g. `prselectbtn_g04b.t32`), or empty.
|
|
pub name: String,
|
|
/// Child magic tag: `T8aD`, `RATC`, `ttcf`, `png`, …
|
|
pub kind: String,
|
|
/// Byte offset of the child (its magic) within the RATC payload.
|
|
pub offset: usize,
|
|
/// Byte length of the child, up to the next child (or end of payload).
|
|
pub size: usize,
|
|
}
|
|
|
|
/// Magic at the start of a RATC bundle.
|
|
pub const RATC_MAGIC: [u8; 4] = *b"RATC";
|
|
|
|
/// Whether `bytes` is a RATC bundle.
|
|
pub fn is_ratc(bytes: &[u8]) -> bool {
|
|
bytes.len() >= 4 && bytes[0..4] == RATC_MAGIC
|
|
}
|
|
|
|
/// Recognized child magics and their display tag.
|
|
fn child_kind(m: &[u8]) -> Option<&'static str> {
|
|
match m {
|
|
b"T8aD" => Some("T8aD"),
|
|
b"RATC" => Some("RATC"),
|
|
b"ttcf" => Some("ttc"),
|
|
b"\x89PNG" => Some("png"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// List the children of a RATC bundle (the self-magic at offset 0 is skipped).
|
|
pub fn parse(bytes: &[u8]) -> Option<Vec<RatcChild>> {
|
|
if !is_ratc(bytes) {
|
|
return None;
|
|
}
|
|
|
|
// Offsets of every recognized child magic (skip the self RATC at 0).
|
|
let mut offs: Vec<(usize, &'static str)> = Vec::new();
|
|
let mut i = 4;
|
|
while i + 4 <= bytes.len() {
|
|
if let Some(kind) = child_kind(&bytes[i..i + 4]) {
|
|
offs.push((i, kind));
|
|
i += 4;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
let mut children = Vec::with_capacity(offs.len());
|
|
for (idx, &(off, kind)) in offs.iter().enumerate() {
|
|
let next = offs.get(idx + 1).map(|&(o, _)| o).unwrap_or(bytes.len());
|
|
children.push(RatcChild {
|
|
name: name_before(bytes, off),
|
|
kind: kind.to_string(),
|
|
offset: off,
|
|
size: next.saturating_sub(off),
|
|
});
|
|
}
|
|
Some(children)
|
|
}
|
|
|
|
/// The nearest name string preceding `off`: the *last* printable run (len ≥ 3)
|
|
/// in the 96 bytes before the child magic. A few record-header bytes usually sit
|
|
/// between the name and the magic, so an exact-adjacency scan isn't enough.
|
|
fn name_before(bytes: &[u8], off: usize) -> String {
|
|
let start = off.saturating_sub(96);
|
|
let window = &bytes[start..off];
|
|
let mut best = String::new();
|
|
let mut run_start: Option<usize> = None;
|
|
let flush = |from: usize, to: usize, best: &mut String| {
|
|
if to - from >= 3 {
|
|
*best = String::from_utf8_lossy(&window[from..to]).trim().to_string();
|
|
}
|
|
};
|
|
for (i, &c) in window.iter().enumerate() {
|
|
if (0x20..=0x7e).contains(&c) {
|
|
run_start.get_or_insert(i);
|
|
} else if let Some(s) = run_start.take() {
|
|
flush(s, i, &mut best);
|
|
}
|
|
}
|
|
if let Some(s) = run_start {
|
|
flush(s, window.len(), &mut best);
|
|
}
|
|
best
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn lists_named_children() {
|
|
let mut b = RATC_MAGIC.to_vec();
|
|
b.extend_from_slice(&[0u8; 28]); // header padding
|
|
b.extend_from_slice(b"logo.t32");
|
|
let t8_off = b.len();
|
|
b.extend_from_slice(b"T8aD");
|
|
b.extend_from_slice(&[0u8; 40]); // some child bytes
|
|
b.extend_from_slice(b"sub.rat");
|
|
let ratc_off = b.len();
|
|
b.extend_from_slice(b"RATC");
|
|
b.extend_from_slice(&[0u8; 8]);
|
|
|
|
let kids = parse(&b).unwrap();
|
|
assert_eq!(kids.len(), 2);
|
|
assert_eq!(kids[0].kind, "T8aD");
|
|
assert_eq!(kids[0].name, "logo.t32");
|
|
assert_eq!(kids[0].offset, t8_off);
|
|
assert_eq!(kids[0].size, ratc_off - t8_off);
|
|
assert_eq!(kids[1].kind, "RATC");
|
|
assert_eq!(kids[1].name, "sub.rat");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_non_ratc() {
|
|
assert!(parse(b"T8aD....").is_none());
|
|
}
|
|
}
|