diff --git a/crates/gc-formats/src/collision.rs b/crates/gc-formats/src/collision.rs index 95046e4..dfaa9de 100644 --- a/crates/gc-formats/src/collision.rs +++ b/crates/gc-formats/src/collision.rs @@ -36,19 +36,39 @@ pub struct CollisionHeader { /// is a size, an offset, or a second block's length, and nothing here /// distinguishes those. pub field_08: u32, + /// The payload's first `u16`, at `+0x40`. + /// + /// Measured on all **19** levels that carry a non-empty payload: the two + /// `u16` at `+0x40` and `+0x42` read 1..74 and 13..112 respectively, with + /// products from 53 to 3,256. Both are non-zero on every one of the 19, + /// and neither exceeds a size a collision grid would plausibly use. + /// + /// Reported as two counts, not as a named grid: nothing measured here ties + /// the product to the payload size or to any per-cell stride, so calling it + /// width and depth would be a reading, not a measurement. + pub first: u16, + /// The payload's second `u16`, at `+0x42`. See [`CollisionHeader::first`]. + pub second: u16, } impl CollisionHeader { /// Parse the 8 meaningful bytes at the head. /// - /// The remaining 0x38 bytes are zero on all 27 levels, but they are not - /// required to be: only the two fields below are load-bearing. + /// The remaining 0x38 bytes are zero on 22 of 27 levels, but they are not + /// required to be -- five carry a large word at `+0x08` -- so only the + /// fields below are load-bearing. + /// + /// `first` and `second` read the payload, not the header, so they are zero + /// here and filled in by [`CollisionHeader::region`], which is the only + /// entry point that knows where the payload starts. pub fn parse(bytes: &[u8]) -> Result { let mut b = Buf::new(bytes); Ok(CollisionHeader { header_size: b.u32("collision header size")?, payload_size: b.u32("collision payload size")?, field_08: b.u32("collision +0x08")?, + first: 0, + second: 0, }) } } @@ -92,6 +112,21 @@ impl CollisionHeader { }, })?; let pstart = start + hsize; + // The payload's own first two u16, read from the bytes past the header. + let lead = core.get(pstart..pstart + 4).ok_or(Error { + offset: pstart, + context: "collision payload is too short for its leading counts", + kind: ErrorKind::OutOfBounds { + needed: 4, + available: core.len().saturating_sub(pstart), + }, + })?; + let header = CollisionHeader { + first: u16::from_le_bytes([lead[0], lead[1]]), + second: u16::from_le_bytes([lead[2], lead[3]]), + ..header + }; + let pend = pstart + header.payload_size as usize; let payload = core.get(pstart..pend).ok_or(Error { offset: pstart, diff --git a/docs/plan/level-assets.md b/docs/plan/level-assets.md index 278ee1a..a6082ef 100644 --- a/docs/plan/level-assets.md +++ b/docs/plan/level-assets.md @@ -1486,3 +1486,36 @@ alignment, and they carry no measurable periodicity. With every slot position taking ~256 distinct values they are dense data -- fat vertices, as the roadmap expects -- and locating the vertex layout inside them needs a reference for this game's instanced-geometry format, not another stride scan. + +## The collision payload opens with two counts + +**Range measured:** the first four bytes of the payload on all **19** levels +that carry a non-empty one. + +**Result:** two `u16` at payload `+0` and `+2`, both non-zero on all 19, ranging +1..74 and 13..112, products 53 to 3,256. + +| level | first | second | product | payload bytes | +|---|---|---|---|---| +| 0 | 11 | 33 | 363 | 903,248 | +| 1 | 24 | 16 | 384 | 2,800,496 | +| 2 | 1 | 53 | 53 | 2,315,296 | +| 7 | 40 | 35 | 1,400 | 4,049,232 | +| 9 | 74 | 44 | 3,256 | 2,518,352 | +| 13 | 2 | 64 | 128 | 3,333,520 | +| 26 | 27 | 112 | 3,024 | 652,176 | + +**Reported as two counts, not as a grid.** They have the shape of collision +grid dimensions -- bounded, both non-zero, products in the hundreds to +thousands -- but nothing measured ties the product to the payload size or to any +per-cell stride. Level 2's `1 x 53` giving a 2.3 MB payload and level 26's +`27 x 112` giving 652 KB shows the product does not size the payload, which +means either cells vary wildly in size or these are not grid dimensions. Naming +them width and depth would be a reading, not a measurement. + +**After the two counts** comes a run of `u16` that stays below 8,192 for 30 to +58 entries depending on level, always an even count, and is not ascending. That +is where the per-cell table is likely to start, and it is the next thing to +resolve. + +Exposed as `CollisionHeader::first` and `::second`.