//! Shrub geometry: the class header. //! //! Spec: `docs/plan/level-assets.md`. //! //! Shrub is the third class-geometry family, and it is not the moby one. All //! 358 shrub spans on the disc have a leading word of 0 or one too large to //! index a table into, so none of them contain a moby-style part table -- the //! rule `vertex_offset + 16 * vertex_count == next_part`, which holds for //! 43,467 of 43,467 moby parts, holds for **zero** of them. //! //! What the shrub header *does* have is two fields that self-identify exactly, //! on all 358 spans: //! //! - `+0x24` equals the owning class entry's own `o_class`. //! - `+0x2c` is a data offset that lands inside the span. //! //! Those two are what this parses. The body past the data offset is not //! decoded: the run of bytes there does not tile at any stride that divides the //! tail consistently with the count at `+0x28`. //! //! Note the class entry size differs here too: shrub entries are 0x30 bytes, //! not 0x20, because they carry billboard texture fields the other two tables //! do not have. use crate::buf::{Buf, Error, ErrorKind, Result}; /// The measured size of a shrub class entry. pub const SHRUB_CLASS_ENTRY_BYTES: usize = 0x30; /// The decoded part of a shrub class header. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ShrubHeader { /// Four floats at `+0x00..+0x10`. All finite on all 358 spans, but they do /// not behave like a `min.xyz / max.xyz` bounding box -- that ordering only /// holds on 146 of 358. Not named because it is not established. pub bounds: [f32; 4], /// A positive float at `+0x10`, 3.0 to 20.0 across the disc. pub size: f32, /// A float at `+0x14`. Exactly 0.0 on 342 of the disc's 358 spans; the /// other 16 hold the bit pattern `04000000`, a denormal of about 2.8e-45. /// /// The Emotion Engine has no denormals, so hardware reads all 358 of these /// as zero. That is the only reading the game itself ever sees, and it is /// why this is safe to treat as a flag rather than a magnitude -- see /// `AGENTS.md` on reproducing the FPU model rather than correcting it. pub reserved: f32, /// The class this geometry belongs to; equals the class entry's /// `o_class` on all 358 spans on the disc. pub o_class: u32, /// A count. Meaning not established. pub count: u32, /// Offset of the geometry data from the start of the span. Lands inside /// the span on all 358 spans on the disc. pub data_offset: u32, } /// Bytes per body record. /// /// Each record is 6 varying bytes followed by 2 constant ones. Found by /// grouping sibling spans and byte-diffing them rather than by stride-fitting, /// which failed three times: the varying payload is **not** sized by the /// `count` field, and was a constant 144 bytes per group whether `count` was /// 1, 2, 3, 5 or 6. pub const SHRUB_BODY_RECORD_BYTES: usize = 8; /// The two constant bytes closing each body record. pub const SHRUB_RECORD_TAIL: [u8; 2] = [0, 0]; /// A shrub span: its header plus the bytes the header points at. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Shrub<'a> { pub header: ShrubHeader, /// The whole span, starting at `+0x24` with the class number. pub raw: &'a [u8], /// The bytes at `data_offset`, running to `span_end`. Not yet decoded. pub data: &'a [u8], /// Whether `span_end` came from the next class rather than from the end of /// the lump. False for the last class in each level, whose span contains /// whatever follows shrub. pub span_end_is_a_successor: bool, } impl<'a> Shrub<'a> { /// The body's 8-byte records. /// /// The payload of each record is six bytes whose encoding is **not** /// established. It is not half-floats -- decoding as a `half3` gives /// values like 245.25 and -168.375 -- and a VIF reading of it was /// retracted after a control put it on the random baseline. Handed back /// raw rather than guessed at. pub fn body_records(&self) -> impl Iterator { self.data.chunks_exact(SHRUB_BODY_RECORD_BYTES) } /// How many whole records the body holds. pub fn body_record_count(&self) -> usize { self.data.len() / SHRUB_BODY_RECORD_BYTES } /// Whether every whole record ends in the constant pair. /// /// This is how a class's body is delimited: on all 24 levels that have /// shrub geometry, the last shrub class's body ends exactly where its /// trailing `00 00` pair stops, always on a record boundary. pub fn body_is_well_formed(&self) -> bool { let records: Vec<&[u8]> = self.body_records().collect(); !records.is_empty() && records.iter().all(|r| r[6..8] == SHRUB_RECORD_TAIL) } } impl ShrubHeader { /// Parse a shrub span starting at `offset` within `core_data`. /// /// `o_class` is passed in so the self-identifying field can be checked /// rather than merely reported: a span whose `+0x24` disagrees with its /// own class entry is a different thing entirely, and quietly returning /// the mismatched value would hide that. /// `span_end` bounds the class span within `core`. It matters: without it /// `data` runs to the end of the lump and swallows whatever follows, which /// is how a per-class check ends up measuring its neighbours. pub fn region<'a>( core: &'a [u8], offset: i32, o_class: i32, span_end: usize, ) -> Result>> { if offset <= 0 { return Ok(None); } let start = offset as usize; let head = core.get(start..).filter(|s| s.len() >= 0x30).ok_or(Error { offset: start, context: "shrub span is too short to hold a header", kind: ErrorKind::OutOfBounds { needed: 0x30, available: core.len().saturating_sub(start), }, })?; let mut fb = Buf::new(&head[..16]); let bounds = [ fb.f32("shrub bounds 0")?, fb.f32("shrub bounds 1")?, fb.f32("shrub bounds 2")?, fb.f32("shrub bounds 3")?, ]; let mut fs = Buf::new(&head[0x10..0x18]); let size = fs.f32("shrub size")?; let reserved = fs.f32("shrub reserved")?; if !bounds.iter().all(|v| v.is_finite()) || !size.is_finite() || !reserved.is_finite() { return Err(Error { offset: start, context: "shrub header holds a non-finite float", kind: ErrorKind::Invalid("shrub header float"), }); } // Zero, or the denormal the PS2 would flush to zero anyway. Anything // else means the header is not laid out the way this thinks it is. if reserved != 0.0 && !reserved.is_subnormal() { return Err(Error { offset: start + 0x14, context: "shrub reserved word is neither zero nor denormal", kind: ErrorKind::Invalid("shrub reserved"), }); } let mut b = Buf::new(&head[0x24..]); let span_class = b.u32("shrub o_class")?; let header = ShrubHeader { bounds: [0.0; 4], size: 0.0, reserved: 0.0, o_class: span_class, count: b.u32("shrub count")?, data_offset: b.u32("shrub data offset")?, }; if header.o_class != o_class.max(0) as u32 { return Err(Error { offset: start + 0x24, context: "shrub span does not name its own class", kind: ErrorKind::Invalid("shrub o_class disagrees with the class entry"), }); } let at = header.data_offset as usize; let stop = span_end.clamp(start, core.len()); let data = core.get(start + at..stop).ok_or(Error { offset: start + at, context: "shrub data offset runs past the lump", kind: ErrorKind::OutOfBounds { needed: at, available: core.len().saturating_sub(start), }, })?; Ok(Some(Shrub { span_end_is_a_successor: true, header: ShrubHeader { bounds, size, reserved, o_class: header.o_class, count: header.count, data_offset: header.data_offset, }, raw: &core[start..], data, })) } } #[cfg(test)] mod tests { use super::*; /// Where the span sits in the fake lump. Offset 0 means "absent", so the /// span cannot start there. const BASE: usize = 0x10; /// A span whose `+0x24` names class 1072 and whose data starts at 0x30. fn span() -> Vec { let mut v = vec![0u8; BASE + 0x40]; v[BASE + 0x24..BASE + 0x28].copy_from_slice(&1072u32.to_le_bytes()); v[BASE + 0x28..BASE + 0x2c].copy_from_slice(&5u32.to_le_bytes()); v[BASE + 0x2c..BASE + 0x30].copy_from_slice(&0x30u32.to_le_bytes()); v[BASE + 0x30..].fill(0xab); v } #[test] fn reads_the_self_identifying_header() { let c = span(); let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len()) .unwrap() .expect("present"); assert_eq!(s.header.o_class, 1072); assert_eq!(s.header.count, 5); assert_eq!(s.header.data_offset, 0x30); assert!(s.data.iter().all(|&b| b == 0xab)); } #[test] fn a_span_naming_the_wrong_class_is_rejected() { let c = span(); let err = ShrubHeader::region(&c, BASE as i32, 999, c.len()).unwrap_err(); assert!(format!("{err:?}").contains("disagrees")); } #[test] fn a_zero_offset_means_absent_not_present() { let c = span(); assert!(ShrubHeader::region(&c, 0, 1072, c.len()).unwrap().is_none()); assert!(ShrubHeader::region(&c, -1, 1072, c.len()) .unwrap() .is_none()); } #[test] fn body_records_split_into_eights_and_check_their_tail() { let c = span(); let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len()) .unwrap() .expect("present"); // The fixture fills 0xab, so every record's tail is wrong. assert!(!s.body_is_well_formed()); assert_eq!(s.body_record_count(), (0x40 - 0x30) / 8); // Fix the tails and it is well formed. let mut c2 = span(); for i in 0..((0x40 - 0x30) / 8) { c2[BASE + 0x30 + i * 8 + 6] = 0; c2[BASE + 0x30 + i * 8 + 7] = 0; } let s2 = ShrubHeader::region(&c2, BASE as i32, 1072, c2.len()) .unwrap() .expect("present"); assert!(s2.body_is_well_formed()); assert_eq!(s2.body_records().count(), s2.body_record_count()); } #[test] fn a_body_shorter_than_one_record_is_not_well_formed() { let mut c = span(); // Point the data at the last four bytes of the span. c[BASE + 0x2c..BASE + 0x30].copy_from_slice(&(0x3cu32).to_le_bytes()); let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len()) .unwrap() .expect("present"); assert_eq!(s.body_record_count(), 0); assert!(!s.body_is_well_formed()); } #[test] fn out_of_range_is_an_error_not_a_panic() { // Offset 0 is absent, not an error -- an empty lump is just empty. assert!(ShrubHeader::region(&[], 0, 1, 0).unwrap().is_none()); assert!(ShrubHeader::region(&[], 4, 1, 8).is_err()); // Data offset past the end of the lump. let mut c = span(); c[BASE + 0x2c..BASE + 0x30].copy_from_slice(&9_000u32.to_le_bytes()); assert!(ShrubHeader::region(&c, BASE as i32, 1072, c.len()).is_err()); // Span starting near the end, with no room for a header. assert!(ShrubHeader::region(&c, (c.len() - 4) as i32, 1072, c.len()).is_err()); } }