Parse the shrub class header
Shrub is the third class-geometry family and is not the moby one: all 358 spans on the disc have a leading word of 0 or one too large to index a table into, so none contain a moby-style part table. The rule that holds for 43,467 of 43,467 moby parts holds for zero of them. Two header fields do self-identify, exactly, on all 358 spans: +0x24 equals the owning class entry's own o_class, and +0x2c is a data offset landing inside the span. Both are checked rather than merely reported -- a span naming the wrong class is a different thing, and quietly returning the mismatched value would hide that. The body past +0x2c is not decoded. It tiles at no stride consistent with the count at +0x28 (tails of 240, 240, 192 and 224 bytes against counts of 5, 6, 5 and 3), and the header's leading bytes are packed rather than float even though +0x10 lands exactly on 5.0f on 281 of 358 spans. Shrub class entries are 0x30 bytes rather than 0x20, because they carry billboard texture fields the other two tables lack; the parser records that so the stride is not re-derived.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
//! 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`, and the header's leading bytes
|
||||
//! are packed rather than float despite `+0x10` landing exactly on 5.0f on 281
|
||||
//! of 358 spans.
|
||||
//!
|
||||
//! 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, Eq)]
|
||||
pub struct ShrubHeader {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// A shrub span: its header plus the bytes the header points at.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
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`. Not yet decoded.
|
||||
pub data: &'a [u8],
|
||||
}
|
||||
|
||||
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.
|
||||
pub fn region<'a>(core: &'a [u8], offset: i32, o_class: i32) -> 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 b = Buf::new(&head[0x24..]);
|
||||
let header = ShrubHeader {
|
||||
o_class: b.u32("shrub o_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 data = core.get(start + at..).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 {
|
||||
header,
|
||||
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<u8> {
|
||||
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, 0x10, 1072)
|
||||
.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).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).unwrap().is_none());
|
||||
assert!(ShrubHeader::region(&c, -1, 1072).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[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).unwrap().is_none());
|
||||
assert!(ShrubHeader::region(&[], 4, 1).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).is_err());
|
||||
// Span starting near the end, with no room for a header.
|
||||
assert!(ShrubHeader::region(&c, (c.len() - 4) as i32, 1072).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user