Battle action exit gates and softlock classes
The action state machine has two states that wait on a condition with no timeout. State 0x51 waits for the party's HP readouts to finish counting; state 0x19 waits for the attacker to come within reach. When the thing each waits on can no longer happen, the battle never advances - and because the camera's idle sweep runs regardless, both look the same on screen: the camera orbits the acting actor forever.
At a glance
0x51 HP-readout park | 0x19 approach park | |
|---|---|---|
| Waits on | settle check FUN_801E7250 answering “settled” | range check FUN_8004E2F0 returning 0 |
| Invariant | displayed HP minus live HP equals the pending accumulator, on every party slot | a clip with root motion is playing while the poll fails |
| Broken by | a displayed / live mismatch left with a zero accumulator | a stale exit-to-idle flag ending the approach clip |
| Retail trigger | a restore's assigning seed landing on a drain still in flight | Kemaro putting a walk-less monster back with the flag still set, then that monster's out-of-reach melee inside one idle cycle |
| Caught in unmodified play | no capture; a pad-reachable trigger is read from the code | yes - the second Gaza fight |
| Self-recovers | on a poison or regen tick of the affected member | never |
| Disc patch | none | legaia-patcher --approach-softlock-fix |
| Port | same conventions, seeds paired at every call site | cannot park: the approach drive does not depend on a live clip |
The 0x51 gate: waiting for the HP readout
The party HUD counts HP down over several frames after a hit, and the Done band exists partly to let that count finish. State 0x50 seeds a countdown; state 0x51 decrements it and leaves when it is negative and the menu flag is clear. The decrement is gated on the settle check: “not settled” skips the store and nothing else. The signature of the park is therefore state 0x51 with the countdown frozen at the value 0x50 seeded.
Only the party side waits. Retail draws no HP readout for monsters; their displayed-HP mirror is maintained but never drawn or animated.
“Party side” is retail's slot numbering: the battle loader seats the party at slots 0..2 and monster k at 3 + k whatever the party size. The port compacts the monster row to sit right after the party, so it maps each slot back to retail's numbering before the settle check and the drain test it. The same map serves every other place the battle code meets retail's numbering (two slot numberings). Without that map a two-member party's first monster read as a party seat, and a monster killed by Nighto's instant death - which zeroes live HP without seeding the readout, harmless in retail - parked the action for good. That park was the port's, not retail's.
H that lands while the readout still has A to fall leaves live HP at L + H and ramps the readout from D to D + H: it settles exactly A above live HP with a zero accumulator. The drain is the only thing that moves the readout and it is guarded off, so every later action targeting that member is told “not settled”.The drain
- Party slot: a quarter of the accumulator per game frame, biased so the step is never zero. A full readout drains in a size-insensitive number of frames - 600 in 20, 1289 in 23, 9999 in 30.
- Monster slot: the whole delta in one frame.
- No max clamp: the drain never reads max HP and the HUD draws the readout raw, so a readout seeded above max is drawn above max.
Where the park can come from
Every writer of the accumulator follows one of the two conventions above. The hit resolver FUN_801EC3E4 accumulates at every site; the item / restore applier FUN_800402F4 assigns at all three of its seeds. The streamed cast modules carry their own HP writers and follow the safe convention.
A menu restore cannot land mid-drain: a party-targeted action holds its own 0x51 open until every party readout settles, so the drain has always finished before the menu can act. The one path that can is the Lost Grail's Final Heal auto-revive, which the Done band runs itself on a member whose HP has just reached zero. Measured across many auto-revives it has always landed on an already-drained accumulator, with a comfortable margin at mid-game HP totals; the margin shrinks as max HP grows, because a bigger readout takes more frames to drain. A second paper trigger is the Point Card aimed at a party member, whose discharge writes live HP and seeds neither side (see the items glitch write-up).
The one re-sync in the battle code is the per-round status ticker, which force-assigns displayed HP to live HP after each of its own writes. A poison or regen tick on the affected member clears the park.
The exit tests the value, not the crossing
Nothing records “the countdown crossed zero this frame”. The exit reads the countdown fresh every pass and holds while the menu flag is up, which also pins a floor of twelve frames. A port that conditions the exit on the crossing loses it for good if the menu flag is up on that one frame.
The 0x19 approach park
State 0x19 recomputes facing, calls the range check, and advances to the strike loop only when it returns in-range. It contains no movement code and no timeout. The movement comes from somewhere else entirely: the per-actor animation tick adds a root-motion term to the actor's position while a clip with a non-zero speed is playing.
The walk states 0x15..0x18 run only for a monster whose action table carries walk tag 0x20. Of the 186 monster records, 180 have none, and fall back to state 0x19 with the generic Move clip staged - so for almost every monster the approach depends entirely on that clip staying alive.
- Kemarothe summon carries its target off and stages its hit reaction there, setting anim-event flag bit 2, “exit to idle at clip end”
- Put-backKemaro restores the target and starts idle through the mid-clip commit, which keeps bit 2: the flag now waits for the idle loop's first end
- Melee beginsinside that window the monster's own turn is a melee from out of reach; state
0x14stages the Move clip - Event committhe Move clip is installed through the mid-clip commit path, which clears bits 0-1 and preserves bit 2
- First loop endabout twelve frames in, the natural-end path sees bit 2, stages idle over the queued clip and clears the bit
- Parkidle has zero root motion: the position freezes out of reach and the poll fails forever
An ordinary hit cannot do this: the action that lands it waits until the struck actor's reaction has ended, and that end clears the bit. Of the cast and summon modules, only Kemaro puts its target back in a way that keeps the bit. The bit then lasts one idle cycle - 360 vsyncs for Gaza 2, whose idle loop is the longest and whose five-frame Move loop is the shortest of the 180 walk-less monsters, which is why the park is met in his fight and almost nowhere else. A parked save reads the flag byte as zero: the commit that killed the clip consumed the bit, so the stale flag is visible only in flight. The causal check re-arms the bit by hand and the clip dies at its first natural end; without it the same re-stage walks in and strikes.
A party attacker's reach comes from a three-entry static table; a monster's is size-scaled. The range check returns “out of range” for any target code of 8 or more, so an all-target action can never satisfy a range poll.
The disc patch
legaia-patcher --approach-softlock-fix (off by default; also offered by the in-browser ROM patcher) replaces nine redundant words in the 0x19 arm - a per-frame facing re-derivation the neighbouring states already do - with a guard: if the staged clip is dead while the poll still fails, set the state back to 0x14. Retail's own arm then re-stages the clip and the monster resumes walking. Healthy approaches never see a dead clip and are unaffected.
The port
The engine walks and cannot reproduce this park. Its approach drive is the same root-motion term, run in every approach state whether or not a clip is playing. Every tag search in the action state machine runs over the monster's real action table, so the 180 records without tag 0x20 take 0x19 as in retail and the six that carry one play their own approach clips. A disc-gated sweep fights every one of the 186 records.
See also
The narrative version of the approach park is the Gaza softlock write-up. The full reference - field tables, both seeding conventions store by store, the Final Heal measurements, the Done-band teardown and the item applier's arms - is in docs/subsystems/battle-action-exit-gates.md.