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>
79 lines
2.4 KiB
Rust
79 lines
2.4 KiB
Rust
//! `LSTA` — a sprite list: a header followed by N inline [`T8aD`](crate::t8ad)
|
|
//! frames concatenated back-to-back.
|
|
//!
|
|
//! A `count` lives at `@0x04`, but a few entries disagree with the actual frame
|
|
//! count, so we walk by the `T8aD` magic instead (robust) and decode each frame
|
|
//! from its slice up to the next frame (or end).
|
|
|
|
use crate::t8ad::{self, T8adImage, T8AD_MAGIC};
|
|
|
|
/// Magic at the start of an LSTA sprite list.
|
|
pub const LSTA_MAGIC: [u8; 4] = *b"LSTA";
|
|
|
|
/// Whether `bytes` is an LSTA list.
|
|
pub fn is_lsta(bytes: &[u8]) -> bool {
|
|
bytes.len() >= 4 && bytes[0..4] == LSTA_MAGIC
|
|
}
|
|
|
|
/// Decode all inline T8aD frames. Frames that don't decode (unsupported T8aD
|
|
/// variant) are skipped. Returns `None` only for non-LSTA input.
|
|
pub fn parse(bytes: &[u8]) -> Option<Vec<T8adImage>> {
|
|
if !is_lsta(bytes) {
|
|
return None;
|
|
}
|
|
|
|
// Offsets of each inline T8aD frame (search past the 4-byte magic).
|
|
let mut offs = Vec::new();
|
|
let mut i = 4;
|
|
while i + 4 <= bytes.len() {
|
|
if bytes[i..i + 4] == T8AD_MAGIC {
|
|
offs.push(i);
|
|
i += 4;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
let mut frames = Vec::with_capacity(offs.len());
|
|
for (idx, &off) in offs.iter().enumerate() {
|
|
let next = offs.get(idx + 1).copied().unwrap_or(bytes.len());
|
|
if let Some(img) = t8ad::parse(&bytes[off..next]) {
|
|
frames.push(img);
|
|
}
|
|
}
|
|
Some(frames)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn t8ad_frame(w: u32, h: u32) -> Vec<u8> {
|
|
let mut b = vec![0u8; 64];
|
|
b[0..4].copy_from_slice(&T8AD_MAGIC);
|
|
b[0x14..0x18].copy_from_slice(&w.to_be_bytes());
|
|
b[0x18..0x1c].copy_from_slice(&h.to_be_bytes());
|
|
b[0x1c..0x20].copy_from_slice(&1u32.to_be_bytes());
|
|
b.extend_from_slice(&vec![0x80u8; (w * h * 4) as usize]);
|
|
b
|
|
}
|
|
|
|
#[test]
|
|
fn walks_inline_frames() {
|
|
let mut b = LSTA_MAGIC.to_vec();
|
|
b.extend_from_slice(&2u32.to_be_bytes());
|
|
b.extend_from_slice(&[0u8; 15]); // header remainder
|
|
b.extend_from_slice(&t8ad_frame(4, 4));
|
|
b.extend_from_slice(&t8ad_frame(2, 3));
|
|
let frames = parse(&b).unwrap();
|
|
assert_eq!(frames.len(), 2);
|
|
assert_eq!((frames[0].width, frames[0].height), (4, 4));
|
|
assert_eq!((frames[1].width, frames[1].height), (2, 3));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_non_lsta() {
|
|
assert!(parse(b"T8aD....").is_none());
|
|
}
|
|
}
|