Parse the moby per-class part table

A moby class span does not start with its geometry. The first u32 is the
offset of a part table, and the table is 16-byte records of vertex
offset, vertex count, an unestablished second count, the next part offset
and flags.

Parts tile: vertex_offset + 16 * vertex_count == next_part. The stride is
never assumed anywhere -- it falls out of that identity, which is what
pins the record size and the field boundaries. The identity holds for
43,467 of 43,467 part records across every moby class on all 27 levels,
and the parser rejects any span that violates it rather than returning a
plausible wrong answer.

Two terminations are needed. A zero vertex_offset ends the run, but 172
records on the disc have a zero vertex_offset and a zero next_part while
still carrying a vertex count. Stopping only on the first leaves 99.6%
instead of 100% and quietly includes a second table as if it were parts.

1,270,802 vertices over 4,560 class spans; 116 spans carry no geometry.

Vertices are 16 bytes of VIF-packed half-float quads cycling through four
distinct words. Handed back raw -- unpacking them is separate work from
finding them, and second_count is recorded but not guessed at.
This commit is contained in:
2026-10-11 02:29:57 +02:00
parent 1f96ef09b9
commit bbb10c678d
5 changed files with 369 additions and 5 deletions
+29 -4
View File
@@ -794,7 +794,32 @@ Class entries are 0x20 bytes in `core_index.bin`, fields
`geom_offset, o_class, unknown_8, unknown_c, textures[16]`; all 238 level-0
moby classes carry `textures == ffff..ff`, i.e. no texture slots.
`geom_offset` points into `core_data`, and the spans tile without overlap. The
data at that offset does not start with a recognisable header -- it opens with
zero words and then packed half-float VIF data (`55 55 15 3e` shows up 4 bytes
in). Not decoded.
`geom_offset` points into `core_data`, and the spans tile without overlap.
The span does not start with the geometry. Its first `u32` is the offset of a
part table, and the table is 16-byte records:
```
+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
```
Parts tile: `vertex_offset + 16 * vertex_count == next_part`. The stride is not
assumed -- it falls out of that identity, which is what pins the record size
and the field boundaries. It holds for 43,467 of 43,467 part records across
every moby class on all 27 levels.
Two terminations are needed, not one. A zero `vertex_offset` ends the run, but
172 records across the disc have a zero `vertex_offset` *and* a zero
`next_part` while still carrying a vertex count; stopping only on the second
is what leaves 99.6% instead of 100%. A zero `next_part` means the record
starts a different table rather than a part.
Totals: 1,270,802 vertices over 4,560 class spans, 116 of which carry no
geometry.
Vertices are 16 bytes of VIF-packed half-float quads, cycling through four
distinct 32-bit words. Not unpacked -- handed back raw.
+8 -1
View File
@@ -205,7 +205,14 @@ Phase 1 hands over the extents; this phase works out what each one contains.
- [ ] `tfrag` — terrain: LOD chains, strips, morph targets
- [ ] `tie` — instanced environment geometry, fat vertices
- [ ] `shrub` — foliage and decorative meshes
- [ ] `moby` — dynamic actors; `moby_anim` — skeletal animation
- [x] `moby` — per-class model geometry. A class span opens with a `u32`
pointing at a part table of 16-byte records; parts tile exactly, with
`vertex_offset + 16 * vertex_count == next_part` holding for **43,467 of
43,467** parts across every class on all 27 levels. That identity is what
makes the record size fall out of the data rather than being assumed.
1,270,802 vertices over 4,560 class spans; 116 spans carry no geometry.
Vertices are 16-byte VIF-packed half-float quads, handed back raw.
- [ ] `moby_anim` — skeletal animation, still open
- [x] `collision` — container framing. A 0x40-byte header self-describes: it
carries its own size and the payload size, and `header + payload` stays
inside `core_data` on all 27 levels. 41,481,520 payload bytes across 19