//! Moby model geometry: the per-class part table. //! //! Spec: `docs/plan/level-assets.md`. //! //! Every moby class points at a span in `core_data`. That span opens with a //! `u32` giving the offset of a **part table**, and the table is a run of //! 16-byte records: //! //! ```text //! +0x00 u32 vertex_offset from the start of the class //! +0x04 u16 vertex_count //! +0x06 u16 second_count meaning not established //! +0x08 u32 next_part //! +0x0c u32 flags //! ``` //! //! The invariant that establishes the layout is that parts tile: //! //! **`vertex_offset + 16 * vertex_count == next_part`** //! //! That is what pins the record size and the field boundaries, because the //! stride is not assumed anywhere -- it falls out of the data. It holds for //! **43,467 of 43,467** part records across every moby class on all 27 levels, //! which is the number that makes this a format rather than a coincidence. //! //! Each vertex is 16 bytes of VIF-packed half-float quads; the payload is //! handed back raw because unpacking it is a separate piece of work from //! finding it. The first word of a class span is not a length, and the data //! does not start at the offset the class points to -- it starts at the //! part table's first vertex block. use crate::buf::{Buf, Error, ErrorKind, Result}; /// Bytes per moby vertex, established by the part-tiling invariant. pub const MOBY_VERTEX_BYTES: u32 = 16; /// Bytes per part-table record. pub const MOBY_PART_RECORD_BYTES: usize = 16; /// One part of a moby model: a run of vertices plus the start of the next. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MobyPart { /// Byte offset of this part's vertices, from the start of the class. pub vertex_offset: u32, /// How many vertices. pub vertex_count: u16, /// A second count. Meaning not established; recorded because it is /// consistently non-zero and clearly not the vertex count. pub second_count: u16, /// Start of the next part, equal to `vertex_offset + 16 * vertex_count`. pub next_part: u32, /// Undecoded. Often zero, sometimes not. pub flags: u32, } impl MobyPart { /// The bytes this part's vertices occupy. pub fn vertex_bytes(&self) -> usize { self.vertex_count as usize * MOBY_VERTEX_BYTES as usize } } /// A moby class's geometry span. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MobyGeometry<'a> { /// The whole class span, exactly as it appears in `core_data`. pub raw: &'a [u8], /// Offset of the part table from the start of the span. pub table_offset: u32, /// The parts, in order. pub parts: Vec, } impl<'a> MobyGeometry<'a> { /// A part's vertices, raw. pub fn vertices(&self, part: &MobyPart) -> Option<&'a [u8]> { let start = part.vertex_offset as usize; self.raw.get(start..start.checked_add(part.vertex_bytes())?) } /// Total vertices across all parts. pub fn vertex_count(&self) -> usize { self.parts.iter().map(|p| p.vertex_count as usize).sum() } /// A class whose span opens with a zero table offset carries no geometry. /// /// 113 class spans across the disc are like this. pub fn is_empty(&self) -> bool { self.parts.is_empty() } } /// Parse a moby class span. /// /// Every part is checked against the tiling invariant, and a span that /// violates it is an error rather than a silently wrong answer: this is /// precisely the assumption that could be wrong without looking wrong. pub fn parse_moby_geometry(raw: &[u8]) -> Result> { let table_offset = u32::from_le_bytes( raw.get(..4) .ok_or(Error { offset: 0, context: "moby class span is too short to hold a table offset", kind: ErrorKind::OutOfBounds { needed: 4, available: raw.len(), }, })? .try_into() .expect("4 bytes"), ); let mut parts = Vec::new(); // A zero table offset means the class has no geometry, which is not the // same as an empty table at offset zero. if table_offset != 0 { let mut at = table_offset as usize; while at + MOBY_PART_RECORD_BYTES <= raw.len() { let mut b = Buf::new(&raw[at..]); let part = MobyPart { vertex_offset: b.u32("moby part vertex offset")?, vertex_count: b.u16("moby part vertex count")?, second_count: b.u16("moby part second count")?, next_part: b.u32("moby part next offset")?, flags: b.u32("moby part flags")?, }; // Both zero terminate. A zero vertex offset is the common one; a // zero next offset is the other end of the run, and without it // this record starts a different table rather than a part. if part.vertex_offset == 0 || part.next_part == 0 { break; } let tiled = part.vertex_offset as usize + part.vertex_bytes(); if tiled != part.next_part as usize { return Err(Error { offset: at, context: "moby parts do not tile", kind: ErrorKind::Invalid( "part tiling: next_part != vertex_offset + 16 * vertex_count", ), }); } if part.vertex_bytes() > raw.len().saturating_sub(part.vertex_offset as usize) { return Err(Error { offset: part.vertex_offset as usize, context: "moby part vertices run past the class span", kind: ErrorKind::OutOfBounds { needed: part.vertex_bytes(), available: raw.len().saturating_sub(part.vertex_offset as usize), }, }); } parts.push(part); at += MOBY_PART_RECORD_BYTES; } } Ok(MobyGeometry { raw, table_offset, parts, }) } #[cfg(test)] mod tests { use super::*; /// A span with a table at 0x40 holding two parts that tile. fn span() -> Vec { let mut v = vec![0u8; 0x400]; v[0..4].copy_from_slice(&0x40u32.to_le_bytes()); // Part 1: verts at 0x80, 28 of them -> next part 0x240. v[0x40..0x44].copy_from_slice(&0x80u32.to_le_bytes()); v[0x44..0x46].copy_from_slice(&28u16.to_le_bytes()); v[0x46..0x48].copy_from_slice(&4u16.to_le_bytes()); v[0x48..0x4c].copy_from_slice(&0x240u32.to_le_bytes()); // Part 2: verts at 0x240, 4 of them -> next part 0x280. v[0x50..0x54].copy_from_slice(&0x240u32.to_le_bytes()); v[0x54..0x56].copy_from_slice(&4u16.to_le_bytes()); v[0x56..0x58].copy_from_slice(&0u16.to_le_bytes()); v[0x58..0x5c].copy_from_slice(&0x280u32.to_le_bytes()); // Terminator. v[0x60..0x70].fill(0); v } #[test] fn walks_the_part_table() { let raw = span(); let g = parse_moby_geometry(&raw).unwrap(); assert_eq!(g.table_offset, 0x40); assert_eq!(g.parts.len(), 2); assert_eq!(g.parts[0].vertex_count, 28); assert_eq!(g.parts[0].second_count, 4); assert_eq!(g.parts[1].vertex_count, 4); assert_eq!(g.vertex_count(), 32); assert_eq!(g.vertices(&g.parts[0]).unwrap().len(), 28 * 16); } #[test] fn a_zero_table_offset_is_an_empty_model() { let mut raw = span(); raw[0..4].copy_from_slice(&0u32.to_le_bytes()); let g = parse_moby_geometry(&raw).unwrap(); assert!(g.is_empty()); assert_eq!(g.vertex_count(), 0); } #[test] fn parts_that_do_not_tile_are_an_error_not_a_wrong_answer() { let mut raw = span(); // Claim 28 vertices but point the next part somewhere else. raw[0x48..0x4c].copy_from_slice(&0x999u32.to_le_bytes()); let err = parse_moby_geometry(&raw).unwrap_err(); assert!(format!("{err:?}").contains("do not tile")); } #[test] fn vertices_running_past_the_span_are_rejected() { let mut raw = span(); // Keep the tiling invariant but push the block past the end. raw[0x44..0x46].copy_from_slice(&400u16.to_le_bytes()); raw[0x48..0x4c].copy_from_slice(&((0x80 + 400 * 16) as u32).to_le_bytes()); assert!(parse_moby_geometry(&raw).is_err()); } #[test] fn a_short_span_is_an_error_not_a_panic() { assert!(parse_moby_geometry(&[]).is_err()); assert!(parse_moby_geometry(&[0, 0]).is_err()); // A table offset pointing past the end yields no parts rather than a // panic: there is nothing there to misread. let g = parse_moby_geometry(&[0, 0, 0, 8]).unwrap(); assert!(g.is_empty()); } }