From 2569cc06abca9b02699cbbc0a92de6569037765a Mon Sep 17 00:00:00 2001 From: ReaperOfVeriod Date: Mon, 12 Oct 2026 02:58:46 +0200 Subject: [PATCH] Decode the collision payload's leading 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 3256. level 0 11 x 33 = 363 payload 903248 level 1 24 x 16 = 384 payload 2800496 level 2 1 x 53 = 53 payload 2315296 level 7 40 x 35 = 1400 payload 4049232 level 9 74 x 44 = 3256 payload 2518352 level 13 2 x 64 = 128 payload 3333520 level 26 27 x 112 = 3024 payload 652176 Reported as two counts, not as a named 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, so 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 8192 for 30 to 58 entries depending on level, always an even count, and is not ascending. That is where the per-cell table most likely starts, and it is the next thing to resolve. Exposed as CollisionHeader::first and ::second. They live in the payload rather than the header, so CollisionHeader::region fills them in and the bare CollisionHeader::parse leaves them zero -- documented at the call site so the difference is not mistaken for an absence. --- crates/gc-formats/src/collision.rs | 39 ++++++++++++++++++++++++++++-- docs/plan/level-assets.md | 33 +++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) 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`.