Parse the level core class tables: moby, tie and shrub

These were ruled out for want of an entry size. The size is knowable
exactly, because each table ends precisely where the next begins:

  moby  238 entries at   192, 32 bytes:  192 + 238*32 =  7808 = tie's
  tie    74 entries at  7808, 32 bytes: 7808 +  74*32 = 10176 = shrub's
  shrub   7 entries at 10176, 48 bytes: 10176 +   7*48 = 10512 = textures

That closes on 27 of 27 levels, which is what makes it evidence rather
than a coincidence: any wrong entry size breaks the chain immediately.
It is the same "does this hold on every level" discriminator the project
already uses for the container pair table.

Across the disc: 5,796 moby, 1,864 tie and 358 shrub classes, plus the
1,595 tfrag textures. Class entries are 32 bytes -- asset offset, class
number, two unknown words and 16 bytes of texture slots -- and a shrub
entry adds 16 bytes of billboard slots for 48.

The header now decodes through to the texture tables rather than
skipping 0x18..0x30 wholesale, which also recovers the moby, tie and
shrub texture ranges. Two more pairs sit between shrub textures and the
textures base offset.

Two things worth recording. ClassEntry::parse read its 16 texture bytes
before its four i32s, because the struct literal that uses them comes
after the loop that advances the cursor -- so every entry was parsed
backwards. Struct literal field order does not reorder the reads that
already happened. And every shrub class on level 0 carries all-zero
billboard slots, so the dimensions are optional rather than something
worth asserting on.

This is the third instance of the same failure shape. The gameplay
extent was "an unidentified compression" because it was parsed
compressed; the tfrag record framing was "unrecoverable" because the
entry size was never known. Both looked like properties of the format
and were properties of the question.

Also records that point lights are a runtime feature rather than missing
work: every one of the 18,102 patches has point_lights == 0xffff and the
eight-slot bank is filled by gameplay, so there is nothing for them to
light in a static level render.

And that PCSX2 is not installed, so the comparison loop cannot be built
yet -- which is why lighting magnitudes stay unverified.
This commit is contained in:
2026-10-10 03:44:47 +02:00
parent b8b4a1a3d3
commit e1105543e4
4 changed files with 313 additions and 14 deletions
+52 -3
View File
@@ -516,6 +516,52 @@ light bank. The three inputs were:
* the **light bank** from the level's `gameplay` extent -- **not working**,
see below.
### Point lights are a runtime feature, not a level-load one
Every patch on every level has `point_lights == 0xffff` (none) and
`dir_lights_one == -1`, checked across all 18,102 patches. The eight-slot point
light bank is filled by gameplay at run time, not read from the disc.
So `point_light_vertex` is not missing work for a static level render -- it
would light nothing. The directional pass *is* the complete lighting for a
level-load frame, and the point-light path belongs with the hero, explosions
and the other things that spawn light.
`dir_lights_one == -1` on every patch also means the per-vertex `select` word
is what selects a light set, never the header field -- which is what the
implementation uses.
### The class tables were not unreadable, only unsized
`core_index` holds moby, tie and shrub class tables at 0x18, 0x20 and 0x28,
followed by the texture tables. They had been ruled out for want of an entry
size, and every field order and stride tried.
The sizes are knowable exactly, because **each table ends precisely where the
next begins**:
- moby 238 entries at 192, 32 bytes each: 192 + 238x32 = 7808 = tie's offset
- tie 74 at 7808, 32 bytes: 7808 + 74x32 = 10176 = shrub's offset
- shrub 7 at 10176, 48 bytes: 10176 + 7x48 = 10512 = the tfrag texture table
- tfrag textures 93 at 10512, 16 bytes: 10512 + 93x16 = 12000 = moby textures
That closes on **27 of 27 levels**, which is what makes it evidence rather
than a coincidence: any wrong entry size would break the chain immediately.
The class entries are 32 bytes -- `{asset offset, class number, two unknown
words, 16 bytes of texture slots}` -- and a shrub entry adds 16 bytes of
billboard slots for 48. Across all 27 levels: **5,796 moby, 1,864 tie and 358
shrub classes**, and 1,595 tfrag textures.
Every shrub class on level 0 carries all-zero billboard slots, so the billboard
dimensions are optional rather than something to assert on.
The failure here is the third instance of the same shape. The gameplay extent
was "an unidentified compression" because it was being parsed compressed; the
tfrag record framing was "unrecoverable" because the entry size was never
known. Both looked like properties of the format and were properties of the
question.
### Why the frame is still dark: the light bank does not parse on RAC2
The reference frames the bank as "a u32 offset at +0x04, then an `i32` count
@@ -613,9 +659,12 @@ the fidelity rule.
- **Lighting magnitudes.** The pass runs and the output is plausible; whether
it matches the game needs comparison against reference frames, which needs
the PCSX2 loop that is still to come. Nothing in this file establishes it.
- **The `core_index` class tables**, still unread after every field order and
stride tried. tfrag, texture and lighting data came from identifying each
structure on its own terms instead.
- **tie, moby and shrub geometry.** The class tables now parse (below), but
nothing reads the geometry they name yet. `offset_in_asset_wad` points into
the level's asset WAD, which is a separate container.
- **Point lights** are spawned by gameplay at runtime and are absent from the
disc -- every one of the 18,102 patches has `point_lights == 0xffff` -- so
there is nothing for them to light until the game runs.
A tfrag can now be turned into positioned, UV-mapped, textured, lit triangles,
told which LOD to draw, and morphed toward its parents at the right rate.