Narrow the collision header to two fields plus a third used by five levels

Range measured: all 27 levels that carry collision, reading the 0x40-byte
header each.

Method: read +0x00, +0x04 and +0x08, then check +0x08..+0x40 for zero.

Result:

  u32@+0x00 is 0x40 on 27 of 27 -- the header is self-sizing
  u32@+0x04 is the payload size, 20 distinct values across the disc
  bytes +0x08..+0x40 zero on 22 of 27

Five levels carry a large non-zero word at +0x08:

  level  7  1248048        level 12  2541616
  level  9  1766832        level 13  3376592
                      level 14  4060640

So the header is not two fields and 56 bytes of padding: there is a third field
most levels leave at zero and five use. Whether it is a size, an offset or a
second block's length is not established, and nothing measured here separates
those readings. Exposed as CollisionHeader::field_08.

Correction: the collision header was previously described as self-describing
across its full 0x40, which is true for 22 levels and overstates the case for
the other five.

Still not decoded: the payload. It contains ascending u32 runs on levels 0 and
2 -- level 0 at +280 with deltas 12, 12, 12 then a jump, level 2 at +13868 with
deltas 204, 204, 200, 196 -- but irregular deltas fit neither a flat table nor a
simple stride, and level 1's apparent run is a byte-shift false positive.
Nothing about the payload is asserted.
This commit is contained in:
2026-10-12 02:49:25 +02:00
parent 51348b214d
commit 5610d173b0
2 changed files with 49 additions and 0 deletions
+11
View File
@@ -27,6 +27,15 @@ pub struct CollisionHeader {
pub header_size: u32, pub header_size: u32,
/// Size of the payload that follows. /// Size of the payload that follows.
pub payload_size: u32, pub payload_size: u32,
/// The word at `+0x08`.
///
/// Measured on all 27 levels that carry collision: the rest of the 0x40
/// header is zero padding on 22 of them, but **5 levels carry a large
/// non-zero word here** -- 1,248,048, 1,766,832, 2,541,616, 3,376,592 and
/// 4,060,640, on levels 7, 9, 12, 13 and 14. Meaning not established; it
/// is a size, an offset, or a second block's length, and nothing here
/// distinguishes those.
pub field_08: u32,
} }
impl CollisionHeader { impl CollisionHeader {
@@ -39,6 +48,7 @@ impl CollisionHeader {
Ok(CollisionHeader { Ok(CollisionHeader {
header_size: b.u32("collision header size")?, header_size: b.u32("collision header size")?,
payload_size: b.u32("collision payload size")?, payload_size: b.u32("collision payload size")?,
field_08: b.u32("collision +0x08")?,
}) })
} }
} }
@@ -108,6 +118,7 @@ mod tests {
const BASE: usize = 16; const BASE: usize = 16;
fn core(header_size: u32, payload_size: u32) -> Vec<u8> { fn core(header_size: u32, payload_size: u32) -> Vec<u8> {
#[allow(clippy::let_and_return)]
let mut v = vec![0u8; BASE + 64]; let mut v = vec![0u8; BASE + 64];
v[BASE..BASE + 4].copy_from_slice(&header_size.to_le_bytes()); v[BASE..BASE + 4].copy_from_slice(&header_size.to_le_bytes());
v[BASE + 4..BASE + 8].copy_from_slice(&payload_size.to_le_bytes()); v[BASE + 4..BASE + 8].copy_from_slice(&payload_size.to_le_bytes());
+38
View File
@@ -1331,3 +1331,41 @@ be small on most levels and is not a count at all.
`gc_formats::sky` parses the header and the sub-objects, and the gated test `gc_formats::sky` parses the header and the sub-objects, and the gated test
asserts the sphere is finite, the radius positive where it is positive at all, asserts the sphere is finite, the radius positive where it is positive at all,
the dimensions non-zero, and the sub-object list non-empty on every level. the dimensions non-zero, and the sub-object list non-empty on every level.
## The collision header is two fields, plus a third on five levels
**Range measured:** all 27 levels that carry collision, reading the 0x40-byte
header each.
**Method:** read `+0x00`, `+0x04` and `+0x08`, then check `+0x08..+0x40` for
zero.
**Result:**
- `u32@+0x00` is `0x40` on **27 of 27** -- the header is self-sizing.
- `u32@+0x04` is the payload size, 20 distinct values across the disc.
- **Bytes `+0x08..+0x40` are zero on 22 of 27**, but five levels carry a large
non-zero word at `+0x08`:
| level | `+0x08` |
|---|---|
| 7 | 1,248,048 |
| 9 | 1,766,832 |
| 12 | 2,541,616 |
| 13 | 3,376,592 |
| 14 | 4,060,640 |
So the header is not simply "two fields and 56 bytes of padding" -- there is a
third field that most levels leave at zero and five use. Whether it is a size,
an offset or a second block's length is not established; nothing measured here
separates those readings. It is exposed as `CollisionHeader::field_08`.
**Correction to the framing description.** The collision header was previously
described as self-describing across its full 0x40, which is true for 22 levels
and overstates the case for the other five.
**Still not decoded:** the payload. It contains ascending `u32` runs on levels
0 and 2 -- level 0 at `+280` with deltas 12, 12, 12 then a jump, level 2 at
`+13868` with deltas 204, 204, 200, 196 -- but the irregular deltas fit neither
a flat table nor a simple stride, and level 1's apparent run is a byte-shift
false positive. Nothing about the payload is asserted.