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 feed cast_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

RegionContentsRecovered by
head table0 to 256 words of in-image addresses - the jump table of one of the module's two switchesleading run of words inside the image, stopping at the first framed function
codethe tick's phase machine, the spawn stager, the capture-class trampolinesframe matching
spawn-record band[i16 model_sel][u16 reserved][move-VM bytecode] particle recordsthe consumer's own pointer-forming instruction
inherited taila byte-identical copy of another extracted image's bytes at the same file offsetmastering 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.