Battle internals
The field-level companion to the battle subsystem: the roster pointer table and actor struct, the battle context, the character and monster records the loader reads, the exact camera, the heap ledger that bounds a formation, the status bits and the party-wipe hand-off. Read the main page first; this one assumes its vocabulary and is mostly tables.
At a glance
- Roster
- 8 × u32 actor pointers at
0x801C9370; slots 0..2 party, 3..7 monsters - Context
- One struct at
0x800EB654, reached through the global pointer0x8007BD24 - Formation
- Four monster-id cells at
0x8007BD0C, a 3-byte party roster at0x8007BD10, per-battle flags byte0x8007BD60 - Character record
0x414bytes per member at0x80084708 + n*0x414(the save-file block)- Monster record
- Decoded archive slot (PROT 867), stat head at
+0x00, mesh at+0x04, texture pool at+0x08 - Heap
- One ~1.23 MB best-fit heap; ~145 KB workable for distinct monster blocks
- Parser
legaia_actor_formats::monster_archive(monster records); engine mirrors inengine-core(battle_seats,MonsterMesh)- Confidence
- Roster, context cursors, seats, camera, heap ledger, wipe path: Confirmed. A few actor-struct fields (
+0x3C/+0x40mid-battle role): Inferred
Roster and actor struct
Every combatant is one actor struct. Slots are filled at setup in formation order; empty seats hold a null pointer. The fields other routines read:
| Offset | Type | Field |
|---|---|---|
| +0x07 | u8 | Per-actor state byte; drives the action state machine |
| +0x1F | u8 | Hit-radius / size byte read by the range check (party sizes from a static table, monster size from the record) |
| +0x34 / +0x38 | i16 | Live world X / Z (Y in the adjacent halfwords, 0 on the flat stage) |
| +0x3C / +0x40 | i16 | Authored seat, stamped at setup and copied into the live position; the b-actor position for the range check. Its mid-battle role is Inferred |
| +0x4A / +0x4C | u8 / ptr | Action-entry count and entry pointer array (each entry: id byte first, AGL cost at +0x74) |
| +0x58 | u8 | Monster size << 5 |
| +0x14C / +0x150 / +0x154 | u16 pairs | HP, MP, AGL (cur, max); mirrored at +0x172..+0x174 |
| +0x158 / +0x15C / +0x160 | u16 | ATK, DEF-high, DEF-low |
| +0x164 / +0x168 / +0x16C | u16 | SPD, INT, initiative key for the current round |
| +0x16E | u16 | Status halfword (below) |
| +0x1BC | ptr | Name string |
| +0x1DA / +0x1DC | u8 | Staged animation channel and its restage counter - every clip goes through this one channel |
| +0x1DD / +0x1DE | u8 | Target slot; action category (0 Arts, 1 Item, 2 Magic, 3 Attack, 4 Spirit, 5 Run) |
| +0x1DF..+0x1F2 | u8[] | Action queue: chosen action id first (also the move-power index), then per-action parameters |
| +0x1EF..+0x1F3 | u8[5] | Hit-reaction clip slots (flinch, knockdown, get-up, block), filled from the record's tagged entries |
| +0x1FA | u8 | Caster's spell iterator, cleared by the renderer at magic sustain |
| +0x230 | ptr | Battle-model TMD (from record +0x04), walked as a 0x1C-stride object table |
The range check between two slots is a euclidean distance from the live positions plus the sum of both size bytes, clamped to a per-actor cap. Slot pointers are the only way any battle routine names a combatant.
Battle context struct
One struct describes the fight in progress. Two byte cursors inside it hand the round back and forth: the menu machine walks +0x06, the action machine walks +0x07. Only the first 32 bytes vary between captures; beyond +0x40 is text-rendering scratch.
| Offset | Type | Field |
|---|---|---|
| +0x00 / +0x01 | u8 | Party count / monster count (the seat-table row selectors) |
| +0x06 | u8 | Command-flow byte: 0x0A intro, 0x1E Begin / Run, 0x28 command ring, 0x3C item, 0x46 magic, 0x50 arts, 0x5A target, 0x78 Auto / Command, 0x6E then 0xFE committed |
| +0x07 | u8 | Action-state byte the action machine switches on (0xFF idle); bands on battle action |
| +0x09 | u8 | Round counter |
| +0x0D | u8 | Per-action camera-angle variant, 0..3 - two independent bits: 1/3 add a half-turn of yaw, 2/3 raise the pitch and drop the camera height. Rolled rand() % 4 at action seed, then narrowed per command category |
| +0x13 | u8 | Acting slot index into the roster table |
| +0x14..+0x1B | u8[8] | Per-action parameters (target slot, sub-action, direction / element, second target) |
| +0x1D | u8 | Action context: 3 for summon and capture, else 0 |
| +0x26 | u8 | Level-up banner UI element id (0x65), or 0. Its non-zero state holds the action teardown open for 0x96 frames instead of 0x3C; see battle action |
| +0x29..+0x2D | bytes | Active spell / move icon glyph escape |
| +0x269 | u8 | Captured Seru id, set by the capture roll |
| +0x279 | u8 | Cast-module phase byte (below) |
| +0x287 | u8 | Scripted-fight flag, derived once at battle init from the per-battle flags byte ((flags >> 5) & 4). It gates the escape roll, both magic-capture audio ducks and the attack-return arm's counter-attack test - a per-battle property, not a per-action one |
| +0x288 | u8 | The counter-attack byte proper - the second term of the attack-return gate |
| +0x28A | u8 | Battle mode - the boss multi-phase gate the AI reads |
| +0xA9..+0xEC | text | Battle message buffer |
| +0x6D6 / +0x6D8 / +0x6DA | u16 | Intro timer, Done-band and summon-band countdown (Theeder, Nighto, Zenoir and Aluru reuse it as their own arm countdown, which holds the band in its sustain state), drifting camera yaw |
Formation cells
Three small globals beside the context carry the formation from the trigger to every later stage:
| Address | Size | Field |
|---|---|---|
| 0x8007BD0C | u8[4] | Monster ids per seat, copied from the encounter row by the entity confirm state. Only the first occurrence of an id streams a record; the setup routine shuffles species order on a 50% coin flip |
| 0x8007BD10 | u8[3] | Party roster (character ids per party slot) - the win-pose staging indexes it by acting slot |
| 0x8007BD60 | u8 | Per-battle flags. Bit 0x80 does double duty: set from the row's header byte it selects the alternate seat family, the particle intro and the no-escape flag; during the fight it is the party-survived latch (below) |
| 0x8007B64A | u8 | Stage id: 0 for every retail fight except Tetsu (1) and Cort (2, then 3 once the first form dies). Non-zero pages a code overlay at extraction 966 + id |
| 0x8007B7FC | u8 | Forced-battle id. Has no writer anywhere on the disc: dev-harness residue that would seed the cells from the id and exit to the debug menu |
Details: the species-order rebuild and why three species is unsafe
Before any monster streams, setup classifies the cells into "the first species" and "the other species", holding the other in a single register, then on a coin flip rebuilds the array as [other × n, first × m]. With two species this is an exact swap. With three, the register is overwritten by each later species, so [a, b, c] rebuilds as [c, c, a]: the middle species vanishes and the last is duplicated - on the other half of the flip all three stream verbatim, which turns an over-budget trio into a probabilistic load hang. The randomizer caps every formation at two distinct species for this reason.
The port runs the same walk at battle entry, on the same rand() stream, so a two-species formation opens with either species in front. The setup that follows draws once more for each body it seats and starts that body's first clip at a random frame ((rand() % frames) << 4 into the anim cursor), which is why three of the same monster never idle in step.
The heap ledger
Everything the loader places lives in one best-fit heap of 1,263,616 bytes (1234 KiB, arena 0x80091800..0x801C6000). The malloc wrapper returns NULL on exhaustion and the monster streamer copies through that pointer unchecked, so an over-budget formation writes a decoded block over kernel RAM and the machine locks. Neither VRAM (each seat owns its own texture column) nor the AI bounds a formation - the heap does. Stage init re-initialises the heap at battle entry, so nothing from the field survives into the fight.
Every node resident once the fight has settled, from an allocator-breakpoint trace with each caller resolved (formation [108,3], Mad Ogre + Demon Fly, on the Karisto world map, one-member party):
| Allocation | Bytes | KiB | Allocated by |
|---|---|---|---|
| Sound resource block | 0x6800 | 26.0 | FUN_8001E1B4 at 0x8001E244 |
| Scene asset buffer (holds the party battle meshes) | 0x62C00 | 395.0 | FUN_8001E1B4 at 0x8001E28C |
| GPU packet double buffer | 0x64000 | 400.0 | FUN_8001E3B8 at 0x8001E3D8 |
| Ordering tables (two, depth 12) | 0x4160 each | 16.3 each | FUN_8001F690 at 0x8001F6F4 / 0x8001F714 |
| Battle-mode extra buffer | 0x1000 | 4.0 | FUN_8001E3B8 at 0x8001E52C |
| Battle context | 0x7A34 | 30.6 | FUN_80055B6C at 0x80055E98 |
| Enemy stager working buffer | 0x2E390 | 184.9 | FUN_800513F0 at 0x80051740 |
| One block per distinct monster | 0xEF7C + 0x101F0 | 59.9 + 64.5 | FUN_800542C8 at 0x8005468C |
Battle SFX bank (bse.dat) | 0x1800 | 6.0 | FUN_8001FA88 at 0x8001FAB4 |
| Battle backdrop TMD | 0x3100 | 12.3 | FUN_8001FE70 at 0x8001FEAC |
| Actor render nodes (party, monsters, two backdrop copies) | 0x9C × 5 | 0.15 each | FUN_80020F88 at 0x80021184, via actor_alloc |
| Intro name-window text nodes | 0x34 × 3 | 0.05 each | FUN_8003541C, from the battle overlay |
Transient during the load: the party-mesh decode temp (0x19000) and the LZS decoder's 0x1014 ring, taken and freed around every decode. After the last allocation the free ring is one node of 17,472 bytes (17.1 KiB), and the nodes, headers and free space sum to the arena exactly.
164,412 bytes (160.6 KiB) remain when the first monster block allocates and the tail after the blocks takes 19,576 bytes (19.1 KiB), so two distinct monster blocks fit in about 141.4 KiB. Retail's largest authored formations cost 124.4 KiB; the three Delilas blocks cost 81-84 KiB each, so any two together overshoot by 22-25 KiB - which is why the Delilas Challenge fields them one per round. Instruments: autorun_battle_heap_ledger.lua (the attributed trace) with battle-heap-ledger.py, autorun_delilas_battle_load.lua (formation install + free-ring walk), battle-heap-walk.py (offline from a save-state).
Where each block lands. The monster loader walks ids in ascending order and the allocator carves each block from the top of the free node, so the blocks stack downward from a fixed end: 0x800B9A50 with one party member seated, 0xA8 (one render node) lower per further member. A block's address is a function of the seated count, the formation's ids and each block's +0x08 cost - 145 placements across 86 retail battle states match - and it decides the byte a four-slot event list reads (monster animation).
Camera and seats
The orbit camera (game mode 0x15) projects a world vertex as screen = H · (R · v + TR) / Z, with the constants below. The base matrix holds 16384 · I - a 4x uniform world scale composed under each actor, which is what makes the small battle meshes project at retail size against the same deep translation the backdrop uses at 1x.
| Constant | Value | Note |
|---|---|---|
| pitch | 32 | 12-bit angle, 4096 = 360° |
| yaw | 0x8007B792 | Orbit azimuth; idles at −4 units per frame |
| TR | (0, 1280, 7680) | Fixed translation; target = world origin |
| H | 256 | GTE projection register |
| base matrix | 0x8007BF10 = 16384 · I | 4x world scale (GTE 4096 = 1.0) |
| ground grid | 28 × 28 cells, pitch 0x200 | GTE-rasterised flat plane, not a mesh |
| dome | two actors from one half-shell mesh | Half turn about Y by default; X mirror for stages listed at 0x80078B50 |
The scale belongs to the battle mode, not to the stage: the backdrop is one more actor drawn through the same base matrix, so a fight whose stage stream does not resolve still draws its actors and effects at 4x under the phase camera. Both play hosts take the scale from one decision, battle_fx::battle_world_scale.
Seats are two static tables of [i16 x, y, z, pad] entries selected by combatant count (party table 0x800775C8, stride 0x18; monster table 0x80077608, stride 0x20, alternate family four rows up). The values are on the main page.
The frame step
Every per-frame battle path - the camera walker, the effect waits, the root-motion term - scales by the frame step DAT_1F800393, and the frame driver FUN_80016B6C rebuilds that byte every frame from what frames cost. The newest frame time (an hblank count) goes into a sixteen-entry ring at 0x80084098; the step is the ring’s maximum binned at 0xF1 / 0x1FF / 0x2D1 into 1 / 2 / 3, else 4, raised to the floor at 0x8007B9D8. A frame of 0x2D0 hblanks or more is stored as 0x2BC but compared raw, so 0x2D0 still reads 3 and anything longer 4.
BattleFrameClock frame, and the ticks between hold. Addresses and the captures that pin the order: battle stage and camera.The battle’s floor is 1, so the step is the load alone, not a property of the game state: most battle captures read 2, while summon close-ups, module casts and the arts input read 3. The port ticks once a vsync, which keeps every per-vsync rate whatever the step, and groups the ticks into frames (BattleFrameClock, two vsyncs by default) for the root-motion carry and the camera; a tween armed over n frames lands in n / step steps. A replay that knows retail’s step for a state installs it, which is how the retail comparison drives summon close-ups.
Outside a summon band the port runs the frame driver’s own law over a cost: a state’s measured base plus what the frame draws. Each live effect part adds about 2 hblanks, each live effect-pool sprite about 1.6, and each after-image ghost a whole re-draw of its body, about .092 hblanks a triangle of the monster’s mesh, so a lone member struck by a special clip with two ghosts and a four-hit spark burst climbs to step 3 as retail’s does. A boss’s ghosts are what decide its breath: Caruban’s 842-triangle body puts a lone Vahn’s Fire Breath frames on the step-3 edge and the flames carry them over, so the camera turns at retail’s rate and the stage’s horizon mist sweeps across the view at the end of the turn as it does in retail. The step a pass raises opens the next frame, never its own.
On PCSX-Redux a battle CD-XA clip an item or a Spirit cues holds the ring at its clamp for the clip’s whole read span, the stop watch below. A summon module’s own voice clip does not: the summon films run frames of 250 to 460 hblanks while it plays, so the arms the arm table leaves to the ring, and the Done band after them, run step 2 or 3 as retail’s do rather than 4.
What a frame costs depends on the machine. The frame end reads the hblanks since its last VSync wait twice: once when the CPU has finished (gp+0x3A8) and once after DrawSync (gp+0x3AC, the ring’s entry). The GPU spends that time drawing the table the pass before built, so the entry is the longer of the CPU’s work and the GPU’s. PCSX-Redux draws for free and reads the CPU alone. Mednafen draws in time, and a model of the GPU built on the v2 GPU’s measured costs, with no constant fitted to either emulator, reads its waits to within 8 hblanks at the median. The GPU binds mostly on a lone member and in a summon’s light arms. The two emulators still disagree on the CPU: PCSX-Redux charges two cycles an instruction, and its CD command waits hold Guilty Cross’s arms at about 1650 hblanks where mednafen’s read 650.
The CD wait is a stop watch. While a voice clip plays, the clip sequencer polls the drive from inside the CD interrupt, issuing the next position query the moment the last one answers, until the head passes the clip’s end. PCSX-Redux answers every command 0x800 cycles after it, so the polls take about two thirds of its CPU and a voiced summon arm costs three times its own work, step 4. A console answers in 0xC4E1 cycles on average, so the same polls take about a sixteenth. Read that way, the summon arms agree with mednafen’s steps on .69 of the band’s vsyncs, against .13 for the PCSX-Redux table. On mednafen’s recorded sound Cort’s Guilty Cross then plays its cues at .98 correlation against .86. The port keeps PCSX-Redux’s timing by default and offers the console’s as an opt-in (LEGAIA_BATTLE_CD_TIMING=console): the recordings the films score against are PCSX-Redux’s, and under the console’s timing the full-game ladder deals its rolls again. Five enemy casts lost sound cues under it, and none to a cue test on the frame step: longer frames change how many passes draw before an arm’s roll, so the roll is dealt again, and held on retail’s draws the cues play again. Of the other two, Glare now keeps its cue, and Aluru Cannon lands one a tick outside the score’s window (battle stage and camera).
The port can price its own frames the same way. An opt-in switch (LEGAIA_BATTLE_GPU_COST=1) has the native window cost every triangle and screen primitive it drew on that GPU model and pushes the longer of that and the CPU table into the ring. It stays off: against the PCSX-Redux films the source can only part the port from retail, since that emulator’s GPU is free, and it does so on the summon flash, where a lone caster’s close-up holds step 2 into it; against mednafen’s own steps it agrees less, because the port draws geometry retail never links (the stage and bodies under the summon flash, 626 polygons against retail’s 48) and lifts light arms over the step-2 line. What separates the port’s summon arms from mednafen’s is the CPU half.
Character record (battle fields)
The party side of the roster is the persistent per-character record - the same 0x414-byte block the save file stores. Battle reads stats, equipment and learned spells from it and writes HP / MP back when the fight ends. The full layout is on save record; the battle-relevant fields:
| Offset | Field |
|---|---|
| +0xC9 / +0xCA | Current AP / bonus AP |
| +0xF4..+0x100 | Active-abilities block, OR'd each frame into the global ability bitmask at 0x80074358 |
| +0x104..+0x110 | HP / MP / AP pairs (live) |
| +0x11A | Stat cap field (clamped to 999) |
| +0x11C..+0x122 | Six stat bytes, incremented on level-up |
| +0x13C / +0x13D..+0x160 | Spell count / spell ids (up to 36) |
| +0x161..+0x184 | Parallel spell-level array |
| +0x185..+0x195 | Displayed-skills list (count-prefixed; item use does an ordered insert, sorted ascending by id) |
| +0x196..+0x19D | Eight equipment slot bytes |
| +0x2A7..+0x2B0 | NUL-padded display name (9 bytes) |
| +0x2B0..+0x37F | Summon stash, stride 0x14: the counter and level of an equipment-granted summon while its item is off |
| +0x6F6 | Persistent status word, seeded into actor +0x16E at battle start and mirrored back per frame |
A per-frame aggregator caps each member's stats at 999 and ORs the abilities block into the global bitmask that every other system reads through one bit-test helper - accessories work by flipping bits, not by editing stats.
Monster record
Each species is one LZS slot of 0x14000 bytes in the monster archive at (id − 1) × 0x14000. The decoded block opens on a stat head; the loader copies stats into the actor and keeps the block pointer in a side table at 0x801C9348 so the spoils routine reads rewards without copying them.
Widths are schematic. The pool is 15 × 16 BGR555 CLUTs (0x1E0 bytes), then a 4bpp page 128 or 256 texels wide × 256 rows.
| Offset | Field | Destination |
|---|---|---|
| +0x00 | Name offset (a ^+letter prefix is the element badge: A Fire, B Thunder, C Wind, D Water, E Earth, F Light, G Dark, H Evil) | actor +0x1BC |
| +0x04 | Battle-model TMD offset | actor +0x230 |
| +0x08 | Texture / CLUT pool offset (also the block's heap cost) | VRAM via the loader |
| +0x0C / +0x0E / +0x10 | HP / AGL / MP | actor +0x14C / +0x154 / +0x150 |
| +0x12 / +0x14 / +0x16 | ATK / DEF-high / DEF-low | actor +0x158 / +0x15C / +0x160 |
| +0x18 / +0x1A | INT / SPD | actor +0x168 / +0x164 |
| +0x1D | Element byte | Affinity matrix row |
| +0x21..+0x23 | Three global spell ids the AI may cast (live when > 1) | AI picker |
| +0x3E / +0x3F | Seru id / catch-rate percent | Capture roll |
| +0x44 / +0x46 | Gold / EXP | Victory spoils |
| +0x48 / +0x49 | Drop item id / drop chance percent | Victory drop roll - every enemy rolls, but at most one item drops per battle |
| +0x4A / +0x4C | Action-entry count / entry offset array | actor +0x4A / +0x4C |
- Action entries. Each entry's first byte is a tag:
2/3/4/5/0xBare the hit-reaction clips (their slot indices land in actor+0x1EF..+0x1F3),0x0C..0x1Fare castable attacks,0x23is special; the AGL cost sits at entry+0x74. The three global ids at+0x21are what appear on screen; the local entry ids only gate the pose. - Texture pool. Uploaded to CLUT row
484 + slotand page column(320 + slot × 64, 256), where the slot is the rank of the monster’s id among the formation’s distinct ids, not its seat; a repeated species shares its slot, and slot 2 is the one a summoned Seru’s texture overwrites. The on-disc CBA / TSB words are nominal and relocated per slot. Width is 128 texels unless the wide flag is set:width = (pool_len − 0x1E0) / 256 × 2. - Steal data is not here - it is a static SCUS table (steal table). The monster viewer reproduces the same relocation.
Status bits
Every ailment is a bit in the actor's status halfword. Two overlay legs set them - the hit leg (kind byte at art descriptor +0x7A) and the cast leg (spell descriptor byte +0x0A) - sharing one kind-to-bit map. Nothing wears a bit off with time: every store that clears one is a cure, a strip or a scripted arm, so Venom, Toxic, Rot and Curse last until cured (round loop). The turn counts the port still keeps on Numb, Sleep and Confuse are its own.
| Status | Kind byte | Retail effect |
|---|---|---|
| Toxic | 1 | Drains min(max_hp/16, 256) per round, never kills; rolls ×7/10; suppresses Venom's tick |
| Numb | 2 | Cannot act; clears when hit |
| Venom | 3 | Drains min(max_hp/32, 128) per round, never kills; rolls ×9/10 |
| Sleep | 4 | Cannot act; wakes when hit |
| Confuse | 5 | Random target on the opposite side; a confused party member is delegated to the AI resolver |
| Curse | 6 | Blocks Magic |
| Stone | 7 | Cannot act or be damaged, counts as defeated; a successful escape restores it (the run band floors 0-HP members at 1) |
| Faint | 8 | KO at 0 HP; revive only |
Bit 0x400 has one setter on the disc, in a cast image: PROT 0955's Kiss of Death miss arm (ori v0,v0,0x400 at 0x801F8CFC). Every other write that touches it is a clear (accessory cure, the per-round waker, item cures, the on-hit strip, battle exit). Rot (kind 5 in the art-record space) sets one random bit of 0x38, greying out the rolled limb's attack command. Full writer inventory: battle.md.
Capture-class cast modules
A spell whose class byte is 'c' is too elaborate for the battle overlay to choreograph itself. The action machine pages a small dedicated module - one PROT entry from 0935..0966, chosen by the record's +1 sub-id - into the side overlay window and re-enters it every frame until it reports done.
| Aspect | Mechanism |
|---|---|
| Paging | Action phase 0x28 routes the action to 0x6E..0x71; the module loads at slot-B base 0x801F69D8. Phase 0x70 ticks it and advances only on a zero return - no timer, so an exit gate that cannot pass is a softlock |
| Phase machine | The tick switches on context +0x279 (0..~0x11, 0xFF = done) - a second phase space under action phase 0x70 |
| Tick ABI | Caster = table[ctx+0x13], victim = table[caster+0x1DD]; clips staged through actor +0x1DA / +0x1DC |
| Damage | One call per hit into a roll wrapper with a power constant baked into the module image, so a capture cast never reads the move-power table for its magnitude. Three apply shapes over the band's 79 wrapper call sites: an unsigned clamp to HP (floor 0, can kill) at 70 of them, a signed clamp to HP−1 (floor 1, never kills, a negative roll heals) at the two whole-row sweep stagers, and an unsigned clamp to HP−1 (never kills and never heals) at seven tick-body sites. PROT 0910's applier picks between the last two per hit, from its own slash counter. In the three decoded exemplars the victim load is hardcoded to seat 0 at every apply site; the sweeps index by their own loop counter instead |
| Exemplars | PROT 958 Blazing Slash (Gi), 959 Megaton Press (Che), 960 Plasma Strike (Lu) |
Module anatomy and the patcher mirror are on cast-module.md.
The party-wipe path
The battle always exits the same way - into MAIN INIT - and the wipe fork lives one mode later. Three latches decide where MAIN INIT sends the game:
- DetectThe end-of-action gate counts living, non-petrified party members; none sets the end signal
0xFEand wipe cause5, and clears the survived latch ·FUN_801E295Cstate0x5A - ExitThe exit selector writes
game_mode = 2(MAIN INIT) wipe or no wipe, with the back-from-battle marker set; it never reads the wipe cause ·FUN_80046A20 - ForkWith the survived latch and story flag 0 both clear, MAIN INIT writes
game_mode = 0x16(CARD), entry context1: the title screen, cursor on Continue ·FUN_8003AEB0
| Latch | Set by | Cleared by | Role |
|---|---|---|---|
0x8007BD60 bit 0x80 | Battle load; victory reward path; successful escape | The end-of-action wipe scans | Party-survived - a wipe is the only battle end that leaves it clear |
Story flag 0 (0x80085758 bit 0x80) | The scene script, before a scripted-loss battle (the Rim Elm ambush) | MAIN INIT on every back-from-battle pass, unconditionally | Scripted-loss latch: the wipe returns to the field like a win |
Story flag 1 (bit 0x40) | MAIN INIT on the survived path | MAIN INIT on the wipe path | Script-readable battle outcome, tested by post-battle scene scripts |
Scripts reach the same hand-off directly through a helper twin of the gate body, called by the field-VM op 4C EA. The mode-18/19 game-over artwork overlay (PROT 0902, 29 TIMs) has no static writer and exits to the debug menu: a dev harness around dev art. Nothing on the reachable path draws "GAME OVER".
How we know
| Function | Address | What it proves | Dump / probe |
|---|---|---|---|
| Stage setup + seats | FUN_800513F0 | Seat tables, seat-to-actor copy, size byte, dome as two actors | ghidra/scripts/funcs/800513f0.txt |
| Monster init | FUN_80054CB0 | Record-to-actor field map, reaction-clip slots | 80054cb0.txt |
| Monster streamer | FUN_800542C8 | Archive slot arithmetic, unchecked copy through the heap pointer | 800542c8.txt; autorun_monster_record_source.lua |
| Texture pool upload | FUN_80055468 | Pool layout, CLUT row and page column per id slot | 80055468.txt |
| Battle setup | FUN_80055B6C | Formation cells, species rebuild, stage-id override, forced-battle-id reader | 80055b6c.txt; autorun_formation_cell_writers.lua |
| Heap | FUN_8002B3D4 / FUN_80017888 | Best-fit heap, NULL on exhaustion, the measured ledger | 8002b3d4.txt; autorun_delilas_battle_load.lua |
| Orbit camera | FUN_80026988 / FUN_80026F50 | Projection constants and base matrix | 80026988.txt; autorun_battle_render_capture.lua |
| Range check | FUN_8004E2F0 | Distance + size-byte hit radius | 8004e2f0.txt |
| Stat aggregator | FUN_80042558 | 999 cap, ability bitmask OR | 80042558.txt |
| Status ticker + appliers | FUN_801E752C / FUN_801EC3E4 / FUN_801E09F8 | Poison tick formulas; the kind-to-bit map on both legs | overlay_0898_801e752c.txt, overlay_0898_801ec3e4.txt, overlay_0898_801e09f8.txt |
| Cast-module tick | PROT 958..960 at 0x801F69D8 | Phase byte, tick ABI, seat-0 victim | 8004ad80.txt (materialiser); autorun_delilas_enemy_cast_watch.lua; cast-module.md |
| Exit selector + wipe gate | FUN_80046A20 / FUN_8003AEB0 | Always MAIN INIT; the CARD fork and its three latches | 80046a20.txt, 8003aeb0.txt; autorun_gameover_mode_writer.lua |
| Forced-battle id | 0x8007B7FC | No store or materialised address in SCUS or any static overlay | capstone store sweep; autorun boss-entry captures |
The save-state diffs behind the roster window and residency claims are on capture notes.