Files
Going-Commando/crates/gc-formats/src/shrub.rs
T
ReaperOfVeriod aa2b14ad66 The shrub mesh geometry is in the class blob after all
The open question from the shrub decode was where the mesh lives: the blob
looked like header, palette and optional billboard, with the 333 classes
without a billboard having nothing to draw. Answering it took the running
game, not the disc.

Diffing a loaded shrub class against its disc bytes in the live PCSX2 (level
1, class 1) shows what the loader rewrites: a table at +0x40 of (offset,
size) pairs, whose offsets are span-relative on disc and absolute in RAM with
the sizes unchanged -- all 21 entries matching offset for offset and size for
size -- plus a relocated palette pointer at +0x2c and a pointer at +0x18,
zero on disc, that asserts the parser enforces.

So the blob is header, packet table, packets, [billboard], palette: the mesh
was here the whole time. Why it was missed is the part worth recording --
data_offset points at the END of the span, so reading the body as
data_offset..span_end accounts for the last few hundred bytes and leaves the
20 KB in front of it unexamined. A correct bound and a body small enough to
look plausible are together enough to hide a region.

Range measured: every shrub span on the disc, 358 over 24 levels, each bounded
by its successor class or the located chunk container.

Method: read count at +0x28, then the entries at +0x40, and require them to
tile -- the first packet at align16(0x40 + count*8), every entry exactly size
past the previous, and the last ending at the palette, or 0x40 short of it on
the 25 spans that carry a billboard.

Result: 358 of 358 tile exactly, holding 2,239 packets. That is what pins the
field meanings: count is the packet count (1..55 on the disc), data_offset is
the palette's offset, and the earlier "the body is not sized by count" note
was true for a reason nobody had understood -- the count sizes the packets.

Shrub::packets() exposes them and returns None rather than a partial list
when a table does not tile. Still open: the packet contents (a VIF reading was
already retracted once, so this needs a control rather than a pattern match),
and the 0x20-byte records the loader points at from +0x18.
2026-10-12 02:02:01 +02:00

765 lines
32 KiB
Rust

//! 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<ShrubNormal> {
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,
}
/// 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<Item = &'a [u8]> {
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<Vec<ShrubPacket>> {
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 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<Item = ShrubNormal> + '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<Option<ShrubBillboard>> {
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<Option<Shrub<'a>>> {
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<u8> {
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<u8> {
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<ShrubNormal> = 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<u8> {
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());
}
}