//! Shrub geometry: the class header, the normal palette, and the billboard. //! //! 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. //! //! The class blob is header, palette, optional billboard, in that order. The //! reference for the shape is R&C1's shrub class (documented in //! `vendor/rerac/docs/formats/shrub_sky_rac1.md`, whose offsets this header //! follows: `o_class` at 0x24, a count at 0x28, the data offset at 0x2c), but //! every field below is either **measured on all 358 spans** or marked as not //! established. Where a field is only parallel to R&C1 and not independently //! verified here, the doc comment says so. //! //! What self-identifies 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. //! - `+0x1c` (billboard offset) is non-zero on exactly the 25 classes whose //! core-index entry carries billboard texture fields, and zero on the other //! 333 -- two independent places, one answer. //! //! 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; /// Normals in the palette. Not a maximum: the body's zero padding follows the /// twenty-fourth entry, so a body of 26, 28 or 30 records holds 24 normals and /// 2, 4 or 6 zero records. pub const SHRUB_NORMAL_COUNT: usize = 24; /// One palette entry: a unit normal as three `s16`, then a zero `s16`. /// /// The three components are a fraction of 32767 -- entry `i` of every body /// satisfies `x^2 + y^2 + z^2` within rounding of `32767^2` on all 358 spans. /// The fourth word is zero on all 9,600 records on the disc, which is what /// makes the record 8 bytes rather than 6. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ShrubNormal { pub x: i16, pub y: i16, pub z: i16, } impl ShrubNormal { /// Parse one 8-byte palette record. pub fn parse(bytes: &[u8]) -> Result { let mut b = Buf::new(bytes); let x = b.i16("shrub normal x")?; let y = b.i16("shrub normal y")?; let z = b.i16("shrub normal z")?; let pad = b.i16("shrub normal pad")?; if pad != 0 { return Err(Error { offset: 6, context: "shrub normal pad", kind: ErrorKind::Invalid("shrub normal pad is not zero"), }); } Ok(ShrubNormal { x, y, z }) } /// Whether the entry is a unit normal, within the format's quantisation. /// /// The palette stores a unit vector scaled by 32768, so an axis-aligned /// normal quantises to `(32768, 0, 0)` -- stored as the `s16` `-32768`, /// because the positive axis clamps instead. The squared length therefore /// lands within `32768^2 - 32767^2 = 65535` of `32767^2`, and that bound /// is tight: the largest deviation on the disc is exactly 65,535. /// /// The margin is what makes this a test rather than a tautology. A random /// `s16` triple has a squared length spread about a *billion* either side /// of `32767^2`, so roughly one entry in fifteen thousand would pass by /// chance; 8,592 of 8,592 real entries pass. pub fn is_unit(&self) -> bool { let n = (self.x as i64).pow(2) + (self.y as i64).pow(2) + (self.z as i64).pow(2); (n - 32767i64.pow(2)).abs() <= 65_535 } /// The normal as a unit vector in `[0, 1]`-ish float terms. pub fn unit(self) -> [f32; 3] { [ self.x as f32 / 32767.0, self.y as f32 / 32767.0, self.z as f32 / 32767.0, ] } } /// The decoded part of a shrub class header. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ShrubHeader { /// Four floats at `+0x00..+0x10`: a bounding sphere read as /// centre (x, y, z) and radius (w), following R&C1's header at the same /// offsets. What is measured here rather than assumed: all four are /// finite, the radius is positive on all 358 spans (1.72..41.57), and the /// centres sit near the class's own origin (-18..15, -23..23, -16..20). /// The earlier reading of this field as a `min.xyz / max.xyz` box was /// wrong -- that ordering holds on only 146 of 358 spans -- because it was /// the wrong shape, not because the bytes were. pub sphere: [f32; 4], /// A positive float at `+0x10`, 3.0 to 20.0 across the disc. R&C1's /// parallel at this offset is `mip_distance`; not independently /// established. pub size: f32, /// A `u16` at `+0x14`: zero on 342 spans, 4 on the other 16. R&C1's /// `mode_bits` is at the same offset. Not established. pub mode_bits: u16, /// A `u16` at `+0x16`, zero on all 358 spans. R&C1's field here is /// `instance_count`, runtime scratch that retail files leave zero. /// /// On the 16 spans where `mode_bits` is 4 the whole word at `+0x14` is an /// `f32` denormal, so the Emotion Engine -- which has no denormals -- reads /// zero for the pair either way. That is the sharpest available statement /// of what the hardware does with this field. pub scratch: u16, /// A `u32` at `+0x18`, zero on all 358 spans. R&C1's `instances_pointer` /// is runtime scratch at the same offset. pub reserved: u32, /// Offset of the [`ShrubBillboard`] record from the start of the span. /// Zero means no billboard; a positive value lands inside the span on all /// 25 spans that have one. /// /// This is the strongest self-identification in the header after /// `o_class`: it is non-zero on exactly the classes whose core-index /// entry carries billboard texture fields (25 of 358 each way), and the /// two answers are produced by different bytes in different files. pub billboard_offset: u32, /// A positive `f32` at `+0x20` (0.0044..1.8 on the disc). R&C1's /// `scale` is the position dequantisation factor at the same offset. /// Not independently established. pub scale: 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. It is the **packet count** -- the length of the table at /// `+0x40` -- which the packet tiling proves (see [`Shrub::packets`]). /// Ranges 1..55 on the disc; unrelated to the palette's length. 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, } /// The billboard record at `billboard_offset`, 0x40 bytes. /// /// The register block matches R&C1's layout field for field -- TEX1 at `+0x10` /// with `K` and `MMIN`, TEX0 at `+0x20`, MIPTBP1_1 at `+0x30`, each carrying /// its own register address word (0x14, 0x06, 0x34) -- and the three floats /// in front of them are the one part that does not carry over: they are far /// too large and too varied to be R&C1's `width`/`height`/`z_ofs`, and they /// repeat byte-identically across levels, which dimensions would not. They /// are handed back unnamed rather than guessed at. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ShrubBillboard { /// Fade distance at `+0x00` (5..1000 on the disc). Following R&C1, the /// loader takes `trunc(fade) & 0xff`, which makes 512 read as 0 -- /// billboard only at any distance. pub fade_distance: f32, /// Three floats at `+0x04..+0x10`. Meaning not established. pub unknown: [f32; 3], /// `TEX1_1` at `+0x10`: the signed LOD bias K in 1/16 units, retail /// -233..-168 on the disc. pub k: i16, /// `TEX1_1` minification filter, 4 on every disc billboard. pub mmin: u32, /// `TEX0_1.data_lo` at `+0x20`: a small index (1..4 on the disc). In R&C1 /// this is an index into the class's texture list. pub tex0: u32, /// The GIF register address words carried behind the three registers, /// 0x14 / 0x06 / 0x34 on every disc billboard. Exposed rather than /// asserted on: they corroborate the layout, but a billboard that carries /// a different register set is still a billboard. pub tex1_addr: u32, /// See [`ShrubBillboard::tex1_addr`]. pub tex0_addr: u32, /// See [`ShrubBillboard::tex1_addr`]. pub miptbp_addr: u32, } /// A shrub span: its header, palette and optional billboard. #[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`. This is the palette /// plus its zero padding. pub data: &'a [u8], /// Whether `span_end` came from the next class or the located chunk /// container rather than from the end of the lump. False only for a span /// bounded by a caller that could not locate either. pub span_end_is_a_successor: bool, } /// The 16-byte header at the front of a mesh packet. /// /// Measured on **every packet on the disc** -- 2,056 of them, across the 334 /// successor-bounded spans -- the header is almost entirely constant: /// /// | bytes | value | packets | /// |---|---|---| /// | 0-3 | `04 04 00 01` | 2056 / 2056 | /// | 4-7 | zero | 2056 / 2056 | /// | 8 | zero | 2056 / 2056 | /// | 11 | `0x05` | 2056 / 2056 | /// /// and every packet's size is a multiple of 16 (2056 / 2056). /// /// **What the loader does to it.** Read back out of the running game's memory, /// the same records have byte 8 holding a **count** (0x0b to 0x29 observed), /// byte 11 holding one of four tag values, and bytes 12-15 holding an absolute /// EE RAM pointer where the disc has a span-relative offset. So bytes 8-15 are /// rewritten wholesale on load, and the count this header does *not* carry on /// disc is the one the `+0x40` table already supplies. /// /// That is why the disc header is worth asserting and the RAM one is not: the /// disc form is a fixed signature, the RAM form is populated state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ShrubPacketHeader { /// The constant leading magic, `04 04 00 01`. pub magic: [u8; 4], /// The tag byte at 11. `0x05` on every packet on the disc; rewritten on /// load to one of `0x00`, `0x20`, `0x30`, `0xbc`. pub tag: u8, } /// The magic every mesh packet opens with, on every span on the disc. pub const SHRUB_PACKET_MAGIC: [u8; 4] = [0x04, 0x04, 0x00, 0x01]; /// The tag byte every mesh packet carries on the disc, before the loader /// replaces it. pub const SHRUB_PACKET_TAG: u8 = 0x05; impl ShrubPacketHeader { /// Parse a packet header, bounds-checked against `bytes`. /// /// Returns `None` unless the magic matches, bytes 4-8 are zero and the tag /// is [`SHRUB_PACKET_TAG`] -- the three invariants that hold on 2,056 of /// 2,056 packets. Rejecting anything else is deliberate: a packet that does /// not match is not one this parser understands, and passing it back /// half-understood is how a wrong reading gets treated as a finding. pub fn parse(bytes: &[u8]) -> Option { if bytes.len() < 16 || bytes[..4] != SHRUB_PACKET_MAGIC { return None; } if bytes[4..9].iter().any(|&b| b != 0) { return None; } if bytes[11] != SHRUB_PACKET_TAG { return None; } Some(ShrubPacketHeader { magic: SHRUB_PACKET_MAGIC, tag: bytes[11], }) } } /// One mesh packet: where it sits in the span and how long it is. /// /// The offsets are relative to the start of the span, like every other /// pointer in a class blob. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ShrubPacket { pub offset: usize, pub size: usize, } impl<'a> Shrub<'a> { /// The palette's 8-byte records, raw. pub fn body_records(&self) -> impl Iterator { self.data.chunks_exact(8) } /// How many whole records the body holds. pub fn body_record_count(&self) -> usize { self.data.len() / 8 } /// The mesh packets, as the packet table at `+0x40` names them. /// /// The table is `count` entries of `(offset, size)`, both relative to the /// span start. What makes it a table rather than a guess is that the /// entries **tile the blob**: the first packet starts at /// `align16(0x40 + count * 8)` -- the table, rounded up to 16 -- every /// entry is exactly `size` past the previous one, and the last ends at the /// palette, or exactly 0x40 short of it on the spans that carry a /// billboard record in between. That holds on **358 of 358** spans on the /// disc, and it is what pins the field meanings: `count` is the packet /// count, and `data_offset` is the palette's offset. /// /// Returns `None` when the entries do not tile, rather than handing back /// a partial list: a table that stops making sense means this is not /// laid out the way this thinks it is. pub fn packets(&self) -> Option> { let count = self.header.count as usize; // A count that cannot be read out of the span is not a count. if self.raw.len() < 0x40 + count.saturating_mul(8) { return None; } let mut out = Vec::with_capacity(count); let mut cursor = (0x40 + count.saturating_mul(8) + 0xf) & !0xf; for i in 0..count { let at = 0x40 + i * 8; let entry = self.raw.get(at..at + 8)?; let offset = u32::from_le_bytes(entry[..4].try_into().ok()?) as usize; let size = u32::from_le_bytes(entry[4..].try_into().ok()?) as usize; if offset != cursor { return None; } cursor += size; out.push(ShrubPacket { offset, size }); } // The packets end where the palette begins, leaving room for the // billboard record when the class has one. let palette = self.header.data_offset as usize; let expected_end = if self.header.billboard_offset > 0 { palette.checked_sub(0x40)? } else { palette }; (cursor == expected_end).then_some(out) } /// The mesh packets' headers, parsed. /// /// Yields `None` for any packet whose header does not match the disc /// invariants, so a caller can tell a bad packet from a good one rather /// than receiving a default. pub fn packet_headers(&self) -> impl Iterator> + 'a { self.packets() .unwrap_or_default() .into_iter() .map(move |p| { self.raw .get(p.offset..p.offset + 16) .and_then(ShrubPacketHeader::parse) }) .collect::>() .into_iter() } /// The palette: the first [`SHRUB_NORMAL_COUNT`] records, parsed. /// /// Returns fewer only if the body is shorter than the palette, which does /// not happen on the disc -- every one of the 358 spans carries all 24. pub fn normals(&self) -> impl Iterator + 'a { self.data .chunks_exact(8) .take(SHRUB_NORMAL_COUNT) .filter_map(move |r| ShrubNormal::parse(r).ok()) } /// Records past the palette. Zero on every disc span: the palette is /// padded out with zero records rather than with live normals. pub fn padding_record_count(&self) -> usize { self.body_record_count().saturating_sub(SHRUB_NORMAL_COUNT) } /// Whether every whole record's trailing pair is zero. /// /// The palette's pad word and the padding records are both zero, so a /// body that satisfies this is whole records; it is how a class's body is /// delimited, and it holds on all 358 disc spans now that each is bounded /// by its successor. 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] == [0, 0]) } /// The billboard record, when the header names one. /// /// The offset is checked against `span_end` by /// [`ShrubHeader::region`] rather than trusted: a non-zero offset that /// runs past the span is a mis-set field, not a billboard. pub fn billboard(&self) -> Result> { if self.header.billboard_offset == 0 { return Ok(None); } let at = self.header.billboard_offset as usize; // `raw` runs from the span start, so the record sits at raw[at..]. let bytes = self.raw.get(at..at + 0x40).ok_or(Error { offset: at, context: "shrub billboard", kind: ErrorKind::OutOfBounds { needed: 0x40, available: self.raw.len().saturating_sub(at), }, })?; let mut b = Buf::new(bytes); let fade_distance = b.f32("shrub billboard fade")?; let unknown = [ b.f32("shrub billboard unknown 0")?, b.f32("shrub billboard unknown 1")?, b.f32("shrub billboard unknown 2")?, ]; // TEX1_1: K, a zero high half, then MMIN, then the register address // word and a zero pad -- a 16-byte GifAdData16 per register. let k = b.i16("shrub billboard K")?; b.skip(2, "shrub billboard K high")?; let mmin = b.u32("shrub billboard mmin")?; let tex1_addr = b.u32("shrub billboard tex1 address")?; b.skip(4, "shrub billboard tex1 pad")?; let tex0 = b.u32("shrub billboard tex0 index")?; b.skip(4, "shrub billboard tex0 data hi")?; let tex0_addr = b.u32("shrub billboard tex0 address")?; // MIPTBP1_1: two zero data words, then the register address word and a // pad. The mip base pointers are filled in at level load, as they are // for every ad-gif in this format. b.skip(4, "shrub billboard tex0 pad")?; b.skip(8, "shrub billboard miptbp data")?; let miptbp_addr = b.u32("shrub billboard miptbp address")?; Ok(Some(ShrubBillboard { fade_distance, unknown, k, mmin, tex0, tex1_addr, tex0_addr, miptbp_addr, })) } } impl ShrubHeader { /// Parse a shrub span starting at `offset` within `core`. /// /// `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 sphere = [ fb.f32("shrub sphere x")?, fb.f32("shrub sphere y")?, fb.f32("shrub sphere z")?, fb.f32("shrub sphere radius")?, ]; let mut fs = Buf::new(&head[0x10..0x18]); let size = fs.f32("shrub size")?; let mode_bits = fs.u16("shrub mode bits")?; let scratch = fs.u16("shrub scratch")?; if !sphere.iter().all(|v| v.is_finite()) || !size.is_finite() { return Err(Error { offset: start, context: "shrub header holds a non-finite float", kind: ErrorKind::Invalid("shrub header float"), }); } // A radius is a radius: non-positive means the header is not laid out // the way this thinks it is. if sphere[3] <= 0.0 { return Err(Error { offset: start + 0x0c, context: "shrub sphere radius", kind: ErrorKind::Invalid("shrub sphere radius is not positive"), }); } // One invariant across all 358 spans: the scratch word at +0x16 is // zero everywhere. A non-zero value means this is not a shrub header. // `mode_bits` at +0x14 is *not* asserted -- it is 4 on 16 of the 358 // spans -- so the field is reported rather than validated. if scratch != 0 { return Err(Error { offset: start + 0x16, context: "shrub scratch", kind: ErrorKind::Invalid("shrub scratch is not zero"), }); } let reserved = u32::from_le_bytes(head[0x18..0x1c].try_into().expect("4 bytes at +0x18")); if reserved != 0 { return Err(Error { offset: start + 0x18, context: "shrub reserved word", kind: ErrorKind::Invalid("shrub reserved word is not zero"), }); } let mut b = Buf::new(&head[0x1c..]); let billboard_offset = b.u32("shrub billboard offset")?; let scale = b.f32("shrub scale")?; if !scale.is_finite() || scale <= 0.0 { return Err(Error { offset: start + 0x20, context: "shrub scale", kind: ErrorKind::Invalid("shrub scale is not positive"), }); } let mut bc = Buf::new(&head[0x24..]); let span_class = bc.u32("shrub o_class")?; let header = ShrubHeader { sphere: [0.0; 4], size: 0.0, mode_bits: 0, scratch, reserved, billboard_offset, scale, o_class: span_class, count: bc.u32("shrub count")?, data_offset: bc.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"), }); } // A billboard that is not inside the span is a mis-set offset. let stop = span_end.clamp(start, core.len()); if billboard_offset > 0 && start + billboard_offset as usize + 0x40 > stop { return Err(Error { offset: start + 0x1c, context: "shrub billboard offset", kind: ErrorKind::Invalid("shrub billboard offset runs past the span"), }); } let at = header.data_offset as usize; 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 { sphere, size, mode_bits, scratch, reserved, billboard_offset, scale, 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 unit direction per palette slot: a spiral over the sphere, so every /// entry is a different unit vector rather than a repeated one. fn fixture_normal(i: usize) -> [f32; 3] { let t = i as f32 / SHRUB_NORMAL_COUNT as f32 * std::f32::consts::TAU; let z = (i % 5) as f32 / 2.0 - 1.0; let r = (1.0 - z * z).max(0.0).sqrt(); [r * t.cos(), r * t.sin(), z] } /// A span naming class 1072, data at 0x30, a positive radius and scale, /// and a 24-entry palette of unit normals. `tail` extra bytes follow the /// palette inside the span: zero ones read as padding records. fn span_with_tail(tail: usize) -> Vec { let mut v = vec![0u8; BASE + 0x30 + SHRUB_NORMAL_COUNT * 8 + tail]; v[BASE + 0x0c..BASE + 0x10].copy_from_slice(&12.5f32.to_le_bytes()); v[BASE + 0x10..BASE + 0x14].copy_from_slice(&6.0f32.to_le_bytes()); v[BASE + 0x20..BASE + 0x24].copy_from_slice(&0.25f32.to_le_bytes()); 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()); for (i, n) in (0..SHRUB_NORMAL_COUNT).map(fixture_normal).enumerate() { let at = BASE + 0x30 + i * 8; for (c, &v_c) in n.iter().enumerate() { let q = (v_c * 32767.0).round() as i16; v[at + c * 2..at + c * 2 + 2].copy_from_slice(&q.to_le_bytes()); } } v } /// A span with two zero padding records after the palette. fn span() -> Vec { span_with_tail(16) } #[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_eq!(s.header.billboard_offset, 0); assert_eq!(s.header.scale, 0.25); assert_eq!(s.header.sphere[3], 12.5); assert!(s.billboard().unwrap().is_none()); } #[test] fn the_body_is_24_unit_normals_plus_zero_padding() { let c = span(); let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len()) .unwrap() .expect("present"); let normals: Vec = s.normals().collect(); assert_eq!(normals.len(), SHRUB_NORMAL_COUNT); assert!(normals.iter().all(|n| n.is_unit()), "palette is not unit"); // The fixture's two trailing zero records are padding, not normals. assert_eq!(s.padding_record_count(), 2); assert_eq!(s.body_record_count(), SHRUB_NORMAL_COUNT + 2); assert!(s.body_is_well_formed()); // The palette repeats no direction, and the padding is not in it. assert!(!s.normals().any(|n| n == ShrubNormal { x: 0, y: 0, z: 0 })); } #[test] fn a_body_of_zeros_is_not_a_palette_of_normals() { let mut v = vec![0u8; BASE + 0x40]; v[BASE + 0x0c..BASE + 0x10].copy_from_slice(&12.5f32.to_le_bytes()); v[BASE + 0x10..BASE + 0x14].copy_from_slice(&6.0f32.to_le_bytes()); v[BASE + 0x20..BASE + 0x24].copy_from_slice(&0.25f32.to_le_bytes()); 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()); let s = ShrubHeader::region(&v, BASE as i32, 1072, v.len()) .unwrap() .expect("present"); // Every record is whole and zero-tailed, but no entry is a normal. assert!(s.body_is_well_formed()); assert!(!s.normals().any(|n| n.is_unit())); } /// A span laid out the way a disc blob is: header, packet table, packets, /// then the palette at the end. fn span_with_packets(palette_at: usize) -> Vec { let mut v = vec![0u8; BASE + palette_at + SHRUB_NORMAL_COUNT * 8]; v[BASE + 0x0c..BASE + 0x10].copy_from_slice(&12.5f32.to_le_bytes()); v[BASE + 0x10..BASE + 0x14].copy_from_slice(&6.0f32.to_le_bytes()); v[BASE + 0x20..BASE + 0x24].copy_from_slice(&0.25f32.to_le_bytes()); v[BASE + 0x24..BASE + 0x28].copy_from_slice(&1072u32.to_le_bytes()); v[BASE + 0x2c..BASE + 0x30].copy_from_slice(&(palette_at as u32).to_le_bytes()); for (i, n) in (0..SHRUB_NORMAL_COUNT).map(fixture_normal).enumerate() { let at = BASE + palette_at + i * 8; for (c, v_c) in n.iter().enumerate() { let q = (v_c * 32767.0).round() as i16; v[at + c * 2..at + c * 2 + 2].copy_from_slice(&q.to_le_bytes()); } } v } #[test] fn the_packets_tile_the_blob() { // Three packets tiling from align16(0x40 + 3*8) = 0x60 to the palette. let palette_at = 0x300usize; let mut c = span_with_packets(palette_at); c[BASE + 0x28..BASE + 0x2c].copy_from_slice(&3u32.to_le_bytes()); let entries = [ (0x60usize, 0x40usize), (0xa0, 0x30), (0xd0, palette_at - 0xd0), ]; for (i, (o, sz)) in entries.iter().enumerate() { let at = BASE + 0x40 + i * 8; c[at..at + 4].copy_from_slice(&(*o as u32).to_le_bytes()); c[at + 4..at + 8].copy_from_slice(&(*sz as u32).to_le_bytes()); } let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len()) .unwrap() .expect("present"); let packets = s.packets().expect("the entries tile"); assert_eq!(packets.len(), 3); assert_eq!(packets[0].size, 0x40); assert_eq!(packets[2].size, palette_at - 0xd0); // The palette is untouched by the packets sitting before it. assert_eq!(s.normals().count(), SHRUB_NORMAL_COUNT); assert!(s.normals().all(|n| n.is_unit())); } #[test] fn a_billboard_record_sits_between_the_packets_and_the_palette() { // With a billboard, the packets end 0x40 short of the palette. let palette_at = 0x300usize; let mut c = span_with_packets(palette_at); c[BASE + 0x28..BASE + 0x2c].copy_from_slice(&1u32.to_le_bytes()); // One entry: the table ends at 0x48, aligned up to 0x50. let size = palette_at - 0x40 - 0x50; c[BASE + 0x40..BASE + 0x44].copy_from_slice(&0x50u32.to_le_bytes()); c[BASE + 0x44..BASE + 0x48].copy_from_slice(&(size as u32).to_le_bytes()); // A billboard offset inside the span, in its last 0x40 bytes. c[BASE + 0x1c..BASE + 0x20].copy_from_slice(&((palette_at - 0x40) as u32).to_le_bytes()); let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len()) .unwrap() .expect("present"); let packets = s.packets().expect("the entry stops at the billboard"); assert_eq!(packets.len(), 1); assert_eq!(packets[0].size, palette_at - 0x40 - 0x50); } #[test] fn a_packet_table_that_does_not_tile_is_rejected() { let palette_at = 0x300usize; let mut c = span_with_packets(palette_at); c[BASE + 0x28..BASE + 0x2c].copy_from_slice(&2u32.to_le_bytes()); // The second entry starts mid-packet, not where the first ended. c[BASE + 0x40..BASE + 0x44].copy_from_slice(&0x60u32.to_le_bytes()); c[BASE + 0x44..BASE + 0x48].copy_from_slice(&0x40u32.to_le_bytes()); c[BASE + 0x48..BASE + 0x4c].copy_from_slice(&0x61u32.to_le_bytes()); c[BASE + 0x4c..BASE + 0x50].copy_from_slice(&0x40u32.to_le_bytes()); let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len()) .unwrap() .expect("present"); assert!(s.packets().is_none()); } #[test] fn a_billboard_record_is_read_and_checked() { let mut c = span_with_tail(0x40 + 8); // The billboard sits directly after the palette. let bb = BASE + 0x30 + SHRUB_NORMAL_COUNT * 8; c[BASE + 0x1c..BASE + 0x20].copy_from_slice(&((bb - BASE) as u32).to_le_bytes()); c[bb..bb + 4].copy_from_slice(&50.0f32.to_le_bytes()); // TEX1: K = -217, MMIN = 4, address 0x14. c[bb + 0x10..bb + 0x12].copy_from_slice(&(-217i16).to_le_bytes()); c[bb + 0x14..bb + 0x18].copy_from_slice(&4u32.to_le_bytes()); c[bb + 0x18..bb + 0x1c].copy_from_slice(&0x14u32.to_le_bytes()); // TEX0: data_lo = 1, address 0x06. c[bb + 0x20..bb + 0x24].copy_from_slice(&1u32.to_le_bytes()); c[bb + 0x28..bb + 0x2c].copy_from_slice(&0x06u32.to_le_bytes()); // MIPTBP1_1: address 0x34. c[bb + 0x38..bb + 0x3c].copy_from_slice(&0x34u32.to_le_bytes()); let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len()) .unwrap() .expect("present"); let b = s.billboard().unwrap().expect("billboard present"); assert_eq!(b.fade_distance, 50.0); assert_eq!(b.k, -217); assert_eq!(b.mmin, 4); assert_eq!(b.tex0, 1); assert_eq!(b.tex1_addr, 0x14); assert_eq!(b.tex0_addr, 0x06); assert_eq!(b.miptbp_addr, 0x34); // The palette is untouched by the billboard sitting after it. assert_eq!(s.normals().count(), SHRUB_NORMAL_COUNT); assert!(s.normals().all(|n| n.is_unit())); } #[test] fn a_billboard_offset_past_the_span_is_rejected() { let mut c = span_with_tail(0x40); // Far past the span's end. c[BASE + 0x1c..BASE + 0x20].copy_from_slice(&9000u32.to_le_bytes()); assert!(ShrubHeader::region(&c, BASE as i32, 1072, c.len()).is_err()); } #[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 a_nonpositive_radius_or_scale_is_rejected() { let mut c = span(); c[BASE + 0x0c..BASE + 0x10].copy_from_slice(&0.0f32.to_le_bytes()); assert!(ShrubHeader::region(&c, BASE as i32, 1072, c.len()).is_err()); let mut c = span(); c[BASE + 0x20..BASE + 0x24].copy_from_slice(&0.0f32.to_le_bytes()); assert!(ShrubHeader::region(&c, BASE as i32, 1072, c.len()).is_err()); } #[test] fn a_nonzero_reserved_word_is_rejected() { let mut c = span(); c[BASE + 0x18..BASE + 0x1c].copy_from_slice(&0x40u32.to_le_bytes()); assert!(ShrubHeader::region(&c, BASE as i32, 1072, c.len()).is_err()); } #[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()); } }