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
In the port
legaia_overlay_images::slot_b_module; the same records are what the engine's cast effect pool stages
Behaviour
the phase machine, entry tables and damage shape are on cast modules

The regions

An image reads, from its first byte, as up to four regions. Each is recovered from the image's own bytes:

file +0 = VA 0x801F69D8 recovered by head table0..256 in-image words leading address run codephase machine, stager, trampolines frame matching spawn-record bandpart records + bytecode the spawn call's pointer inherited tailan earlier image's bytes byte match own-content cut
The idealised order, from file start down; on the right, how each region is found. Real images can break the order (below).
RegionContentsRecovered by
head table0 to 256 words of in-image addresses: the jump table of one of the module's two switches. The sltiu bound at the dispatcher says which.the leading run of in-image words, stopping at the first framed function
codethe tick's phase machine, the spawn stager 0x801F6734, the capture-class trampolinesframe matching
spawn-record bandparticle / part records the spell spawnsthe consumer's own pointer-forming instruction
inherited taila byte-identical copy of an earlier image's bytes at the same file offset - mastering residuesee cast modules

Frame matching

A body opens at addiu sp, sp, -F and closes at the first jr ra whose delay slot is addiu sp, sp, +F for the same F. That survives an early jr ra inside a body and a jr ra word sitting in data. What it cannot see is a frameless leaf - no prologue, nothing to match - and the head jump table is what names those. PROT 0949 is the case: its stager forms a table base six words into the image's address run and bounds it with sltiu 0x8, so it owns eight arms, seven of them 20-byte frameless leaves (one eight-step tint ramp on the victim). Read the dispatcher's table base and bound before reading the frame partition.

Not always head-to-tail

PROT 0943 (Curse) and PROT 0961 (Dead End Crisis) carry framed code above their record band. Those bodies sit above each image's own-content cut and are byte-identical to PROT 0942 / 0960 at the same offsets - inherited residue, not the image's own routines. Either way, a record claim is cut at the next function's prologue, never run to the image end.

The spawn-record band

The record format is the summon part record shared with the PROT 0898 effect prototypes:

FieldMeaning
model_sel-1 = a transform node; a small library index = a modelled part; 0x4000 / 0x4001 = the two render-mode nodes the spawn helper special-cases
reservedNever read: the spawn helper loads only the first halfword, and the move VM starts the program at halfword index 2. Zero in every record.
bytecodeThe part's move-VM program

How a record is found

A record is named by an instruction, never by a statistic. The module builds the record's address 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 itself computes - no length field, no terminator scan.

  1. Find every spawn calla jal to either helper, inside a framed body of this image (an inherited tail carries a sibling's calls).
  2. Read $a2 backwards from the delay slota lui/addiu pair above the call or completed in its delay slot; a copy from a saved register (move a2,s0); or a switch whose arms each load $a2 before jumping to one shared call.
  3. Keep only addresses inside the imageplus guards that a pointer is not inside a framed function and its model_sel is one the helper dispatches.
  4. Sort and cuteach record ends where the next begins, or at the next function prologue.

Bounding the highest record

Nothing above the highest record computes an address, so its own program bounds it: walk the move-VM opcode widths to a terminator - 0x08 HALT, or an armed 0x19 / 0x1B idle loop with no HALT right after - and round up to 4, since records are word-aligned. Only where neither turns up does a 0x09 WAIT with the maximal operand 0x0FFF (over a minute; no spell part lasts that long) serve as the bound.

Above it the bytes often keep reading as [header][program], and the walk chains them, stopping at eight zero bytes - the padding below an inherited tail, which would otherwise walk as a legal mesh-0 record and run into the donor's records.

How well the walk reproduces the measured extents

Where the band itself bounds a record, the chained walk reproduces the extent exactly almost every time and never overruns a measured end; the few misses stop below it. So the rule never claims bytes the band does not bound. The program counter can still step past an end on a mis-stepped width before dying on a non-opcode - the claim is what is safe, not the walk. Of the five pointer filters, only two fire on this disc: the address-inside-image test and the call-site-inside-a-framed-body test. The others are guards.

Why the band matters to the coverage tools

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. Naming the band with a parser takes those bytes out of the code denominator. See byte accounting and disc coverage.

See also