Commit Graph
7 Commits
Author SHA1 Message Date
ReaperOfVeriod ed7e568231 Find a ten-entry table on a 148-byte stride after shrub
Range measured: level 0, 16047040..16737344, the 690,304 bytes between the
last shrub class's body and the constant trailer. Nothing outside that range
is read.

Method: reading the bytes at +0x20 as u32 shows ten consecutive entries
whose low halves increase by a constant 0x94 (148), followed by a zero. The
high half is 0xf000 for the first nine and 0x0a40 for the tenth, so the
table is ten entries and ends at +0x48, absolute 16047112.

What the entries mean, what 148 refers to, and why there are ten are all
unestablished. Ten matches no count already known for this level -- 238
moby classes, 74 tie, 7 shrub, 93 tfrag textures -- so it is not yet tied
to anything else in the lump.

Recording it as a lead rather than a finding. The stride is a real
measurement on a bounded range, but a ten-entry table with an unexplained
stride is exactly the shape that looks like a structure while being
something else, and the rule in AGENTS.md is that a pattern gets looked at
before it gets named.
2026-10-11 03:15:55 +02:00
ReaperOfVeriod 352a4e5ad7 Locate the region after shrub, 690 KB on level 0
Range measured: each level's last shrub class data_offset field (class +
0x2c) through to the start of the constant trailer. Nothing outside that
range is read.

Method: the last shrub class's body is an array of 8-byte records whose
final two bytes are 00 00. Scanning forward in 8-byte steps for the first
record whose trailing pair differs locates the body's end. That is a
search for a stated pattern rather than a statistic, so it either finds
the boundary or says it did not.

Result: on 24 of 24 levels the boundary lands exactly on an 8-byte record
boundary, with body lengths of 192, 208, 224 or 240 bytes -- the same sizes
seen when shrub classes were grouped and diffed.

Level 0:

  last shrub class data   16046800 .. 16047040       240 bytes
  next region             16047040 .. 16737344   690,304 bytes
  constant trailer        16737344 .. 16969296   231,952 bytes

So 690,304 bytes between the last shrub class and the trailer are a
distinct region rather than shrub data. What is in them is not established.
They are the leading candidate for moby_anim, now that collision..moby has
been ruled out as texture.

Worth recording how a wrong conclusion nearly happened here. An earlier
probe searched for PS2 NaN filler at the start of this range, found none,
and was about to report the body as having zero length. That would have
been wrong: the last shrub class is a real class with real data, and NaN
filler only appears in degenerate ones. Checking the actual byte pattern
instead of concluding from a failed heuristic is what caught it.
2026-10-11 03:14:28 +02:00
ReaperOfVeriod 8d57a09b76 Bound the last region by core_data's constant trailer
Comparing all 27 levels byte-for-byte from the end: the last 231,952 bytes
(0x38A10) of every level's core_data are identical. Level-specific content
therefore ends at len - 0x38A10, which is a real bound for the final region
and is not core_data.len().

This closes shrub. Across the 24 levels that have a shrub region the bound
gives 24,442,928 bytes total, and the disc test asserts on every level that
the region ends strictly inside the lump rather than at its end.

What the trailer is remains unestablished -- fixed data, or padding from a
fixed-size decompression buffer -- but it is constant, which is all a bound
needs.

The data between the last shrub class and the trailer is high entropy: 183 to
256 distinct byte values per 4 KB block, mean 254, where texture data sits
under 200. It is real content of unknown type and is the most likely home for
moby_anim, now that the range between collision and moby has been ruled out
as texture.

A lump shorter than the trailer now yields end: None rather than a backwards
bound, covered by a test.
2026-10-11 03:09:21 +02:00
ReaperOfVeriod 8882d8fc26 Re-scan at finer granularity: collision..moby is entirely texture
The "3 of 21 blocks not texture-like" figure from the last commit was an
artifact of sampling every 256 KB and missing everything between. Re-scanned
at 4 KB granularity there are about 100 such blocks, and looking at them
rather than only counting them, they are smooth gradient ramps -- sky and
fog texture. A noisy gradient has a high distinct byte count without being
code, which is exactly the mistake the byte-count heuristic invites.

So collision..moby is entirely texture data. There is no separate moby_anim
region there, and two claims about this range have now been wrong: the
"6.2 MB of unknown data" and the "3 unexplained blocks". Both came from
sizes and counts taken without looking at what they described.

moby_anim is therefore still unlocated, but it is not here. The remaining
candidate is the unresolved tail after the last shrub class -- ~925 KB of
real, non-zero data on level 0 that nothing accounts for.

Recording the two corrections together rather than one at a time, because
the mechanism is the same in both: a heuristic count is not an
identification.
2026-10-11 03:06:07 +02:00
ReaperOfVeriod db414b3d5f Correct: the gap between collision and moby is texture data
The previous commit claimed ~6.2 MB of unidentified data sat between the
collision header and the first moby class, and speculated it might be
moby_anim. That was wrong, and it came from quoting a size before bounding
the thing it measured.

Looking at the bytes rather than only the offsets: the range is dominated by
8-bit indexed texture data. Byte-value concentration distinguishes them
cleanly -- a texture block draws on a small palette, code and collision data
does not. 18 of 21 sampled 4 KB blocks fall under 200 distinct byte values,
against 249 for tfrags and 209 for collision.

The real layout on level 0:

  collision  2615232 .. 3518544   (header plus its declared 903,248 payload)
  textures   3518848 .. ~8832192
  moby       8832192 ..

The texture table's data_offset values stop at 5326208, which is what made
the gap look genuine. Mip chains extend well past the last entry's offset,
so sizing a region from a table of first-level offsets understates the data
it points at.

Three of the 21 blocks are not texture-like, so something is mixed in that
is still unidentified. It is not 6.2 MB of moby_anim.

This is the third number this session that looked load-bearing and turned
out to rest on an unbounded measurement. The pattern is consistent enough
to be worth stating plainly: sizes derived from tables or offsets are not
evidence until the bytes underneath have been looked at.
2026-10-11 03:05:07 +02:00
ReaperOfVeriod 8b98d388f7 Show region bounds in probe, which exposed an unnamed 6.2 MB region
Printing the region table next to the header fields makes the gap visible
without having to remember to consult core_layout, and it paid for itself
immediately.

On level 0 the collision region comes out 6216960 bytes, from 2615232 to
the first moby class at 8832192. Collision's own header declares a payload
of 903,248. So roughly 6.2 MB sits between the collision header and the
first moby class, and almost none of it is collision.

The successor rule is correct -- a region does end where the next one
begins -- but "the next" is not yet known there. collision..moby is
really collision + <something unnamed> .. moby. The name is wrong, not
the bound.

Candidates for the space, rough order of likelihood: moby and tie model
animation data, which is what moby_anim would need, then volumes, then
per-class runtime structures. Tfrag texture texels are already placed and
sit earlier in the lump.

Worth resolving before starting moby_anim, since that format is the most
likely tenant. Until then, any probe reading between the collision header
and the first moby class is reading something the region table does not
describe.
2026-10-11 03:03:46 +02:00
ReaperOfVeriod cb38b8c279 Compute core_data region bounds once, from successor boundaries
Region spans were being re-derived per format while probing, usually
running from one known offset to the next with the last span running to
core_data.len(). On level 0 that gave the final shrub class a 925,008-byte
span absorbing most of the lump's tail, and every aggregate computed over
it -- record counts, value distributions, "this looks random" -- was
silently wrong. Nothing errored, which is what made it survive two
commits and a correction.

core_layout computes the layout once: each region ends where the next one
begins, class regions start at the lowest non-zero geometry offset in their
table, and a region whose successor is unknown gets end: None rather than a
clamped guess. Zero class entries mean absent, not position zero -- the
same trap occlusion and collision each fell into separately.

The disc test asserts what the module is actually for: that the unknown is
visible. Shrub's end is unresolved on the 24 levels that have a shrub
region, and that is asserted rather than hidden.

It also found that not every level has every region -- level 10 has neither
occlusion nor shrub -- so the test checks ordering against the canonical
sequence instead of demanding an identical set.

Still open: where the shrub region ends. Texture texels cannot bound it;
they sit before moby, not after shrub. See docs/formats/core-data-regions.md.
2026-10-11 03:02:02 +02:00