Slot-B module image layout Confirmed
Sixty-four PROT entries hold the choreography of every Seru summon and every capture-class cast - one file format wearing 64 different spells. They all load at the same address, one at a time, into the second overlay window. This page is the file: which bytes of one of those images are a jump table, which are code, and which are the particle records the spell spawns.
At a glance
- Where
- PROT.DAT entries
0903..0966, one per spell slot - Load base
0x801F69D8- the shared slot-B overlay window; only one image is resident at a time- Regions
- head jump table, frame-matched code, spawn-record band, inherited mastering tail
- Parser
legaia_asset::slot_b_module; the records feedcast_effect_pool- Behaviour
- the phase machine, the entry tables and the damage shape are the behaviour side, in
docs/subsystems/cast-module.md
The regions
| Region | Contents | Recovered by |
|---|---|---|
| head table | 0 to 256 words of in-image addresses - the jump table of one of the module's two switches | leading run of words inside the image, stopping at the first framed function |
| code | the tick's phase machine, the spawn stager, the capture-class trampolines | frame matching |
| spawn-record band | [i16 model_sel][u16 reserved][move-VM bytecode] particle records | the consumer's own pointer-forming instruction |
| inherited tail | a byte-identical copy of another extracted image's bytes at the same file offset | mastering residue: the build buffer still held the previous image |
Frame matching finds where a body ENDS, not where every body starts. A frameless leaf has
no prologue for the scan to catch, and the jump table is what names it. PROT 0949 is the case: its stager forms
a table base with lui 0x801F; addiu 0x69F0 and bounds it with sltiu a1, 0x8, so it owns
eight arms six words into the image's leading address run - and seven of them are 20-byte frameless leaves that
the frame partition misses. They are one eight-step ramp on the victim actor (tint blend 0x200..0x1000 against
anim rate 7..0), and the 140-byte run they occupy read as "the module's own data region" until the dispatcher's
own table base was read. Read the consumer's pointer-forming instruction before counting table words.
They are not laid out head-to-tail. PROT 0943 (Curse) and PROT 0961 (Dead End Crisis) both carry framed code above their record band, so "everything past the last function is data" swallows five bodies. Those five are real routines but not those images' own: they sit above each image's own-content cut (0943's bytes end at file +0x1037, 0961's at +0x1918) and are byte-identical to PROT 0942 and PROT 0960 at the same offsets - inherited residue. A record claim is therefore cut at the next function's prologue, never run to the image end.
The spawn-record band
A record is named by an instruction, never by a statistic. The module materialises the
record's address with a lui/addiu pair and passes it in $a2 to the actor
spawn helper FUN_80021B04 or its pool wrapper FUN_80050ED4. One record's end is the
next record's start, so both ends of a claim are addresses the module's own code computes - no length field, no
terminator scan, no entropy test.
The pointer is not always a pair sitting just above the call, and a resolver that only reads that shape
misses records the module plainly hands over. The walk reads $a2 backwards from the call's
delay slot and follows three more shapes: the pair completed in the delay slot itself (the
narrow reading saw only the bare high half there, which lands outside the image - 52 of the 97 pointers it
used to drop as "a neighbour's record" were this shape); a copy from a saved register (move a2,s0,
followed up to 256 words back, since no call can clobber a saved register); and switch arms that
each load $a2 in the delay slot of a jump to one shared call. The Rust walker and the Python mirror
the coverage scripts use resolve all three the same way, record for record.
Five filters keep a spurious pointer out, and two of them fire on retail: the resolved address must land
inside this image (55 values), and the call site must lie inside a framed body of this image
(7 values - an inherited tail carries a sibling's spawn calls, and in PROT 0909 three of them name PROT 0908's
records). The other three - a call crossed while following a caller-saved register, a target inside a framed
function, and a model_sel the spawn helper would not dispatch - never fire on this disc, and saying
so is the point: they are guards, not findings. With all five, no claim in the band overlaps a routine the dump
corpus places in the same image.
The image's highest record has nothing above it computing an address, so the band cannot
bound it - its own move-VM program does. Walk the opcode widths to a terminator (0x08 HALT, or an
armed 0x19 / 0x1B idle loop that no HALT immediately follows) and round the end up to
4, because the records are word-aligned. That alignment step is the whole difference between reproducing a
record's measured extent and missing it by exactly 4 bytes, which is what an earlier reading recorded as
evidence that no static bound existed.
A third word bounds a record only where neither of those turns up: a 0x09 WAIT carrying the
maximal operand 0x0FFF. 4095 frames is over a minute, which no cast or summon part is on screen
for, so nothing above it ever runs - but that is a layout argument rather than a VM one, and the band emits the
same operand mid-program too. Ending the walk at the first one costs 48 of the extents the HALT rule reproduces
exactly, so the walk remembers the last one it stepped over and returns it only where it would otherwise have
no bound at all. Under that ordering every image on the disc that has a highest record bounds it.
The residue has one direction, which is what makes the rule safe to consume - but the direction belongs to the claim, not to the walk's program counter. Of the 1238 extents the band itself bounds, the chain reproduces 1231 exactly, not one chain overruns a measured end, and the seven that miss stall below it. So the rule never claims bytes the band does not bound and cannot retire real code from the dump worklist.
The program counter does run past the end, though. Most misses stop exactly on the measured end - the record whose last instruction is non-terminating, the shape the rule was written for - a handful stop below it, and fourteen step past it before dying on a halfword that is no opcode, having mis-stepped a width somewhere inside the record. "Every miss stalls below the end" describes the first two groups only.
Above the highest pointer-credited record the bytes often keep reading as [header][program], and the
walk chains them. It stops at eight zero bytes: a zero header is a legal mesh-0 record and a zero
halfword a legal opcode, so the padding below an image's inherited tail walks as a record and the chain runs on
into the donor's. No pointer-credited record on the disc opens with eight zeros, and twelve chained ones did -
every one of them that padding. PROT 0944 is the worked case: its chain used to walk on through PROT 0942's
records, which put its tail cut 1412 bytes too high.
Why it was read as un-dumped code
The coverage instrument's data-segment rule rejects any run holding a lui of a RAM page. A
record's body is move-VM bytecode - arbitrary bytes - so a long enough band contains that word by accident, and
the opcode statistic then scored the records as code. Several images' whole record bands ranked as
disassembly work on the strength of that. Naming the band with a parser takes them out of the code denominator
and off the worklist. See byte accounting and
disc coverage.