//! `LSTA` — a display list: a header followed by N inline elements concatenated //! back-to-back, each either a [`T8aD`](crate::t8ad) sprite or a `PRMD` //! primitive (a flat coloured quad — the same primitive the UI bundles use to //! dim a scene). //! //! The `count` at `@0x04` is **exact, and counts both kinds**: across all 64 //! lists on the disc, `count == T8aD frames + PRMD primitives` with no //! exceptions (measured 2026-08-11). An earlier note here said "a few entries //! disagree with the actual frame count" — they do not; that comparison was //! counting sprites against a total that includes primitives. //! //! [`parse`] walks by magic and returns the **sprites**, deliberately skipping //! `PRMD` entries, so its result length is `count` only for lists that hold no //! primitives. Six lists do (in `GP_DEBRIEFING_PILOTLOG`, `GP_MISSION_SELECT`), //! each with exactly one. 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 the inline T8aD sprites, skipping `PRMD` primitives. Returns `None` /// only for non-LSTA input. Every one of the 1 281 sprite frames on the disc /// decodes (measured 2026-08-11, after the T8aD rectangle-list fix). pub fn parse(bytes: &[u8]) -> Option> { 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::*; /// A faithful one-rectangle T8aD frame: base header, a 1-entry offset table, /// then the rectangle header (dst 0,0, size w×h) and its pixels. (Before /// 2026-08-11 this fixture wrote no offset-table entry at all and the decoder /// read "pixels" from inside the header — the test only ever checked the /// dimensions, so it passed anyway.) fn t8ad_frame(w: u32, h: u32) -> Vec { let mut b = vec![0u8; 0x2c]; 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(&0x30u32.to_be_bytes()); // offset table → rect at 0x30 for v in [0u32, 0, w, h] { b.extend_from_slice(&v.to_be_bytes()); // dst X, dst Y, width, height } 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()); } }