At a glance

Driver
FUN_801D0748 in the battle overlay (PROT 0898, base 0x801CE818)
State
One byte, ctx[+0x06]. Dispatch is a comparison tree at 0x801D0C84..0x801D0DC8, not a jump table; 22 live cases, everything else falls to a default
Hand-off
State 0xFE stores ctx[+0x06] = 0xFF and ctx[+0x07] = 0: the action SM takes the frame
Hand-back
The action SM's round end stores ctx[+0x06] = 0x14 and bumps the round index ctx[+0x28A]
Input
The packed pad word: Left 0x8000, Right 0x2000, Down 0x4000, Up 0x1000; confirm and cancel masks at 0x800846D0 / 0x800846D4
Stage overlays
PROT 967 (tutorial hook FUN_801F6B70), 968 (Cort arrival), 969 (Cort form change), all ticked by the SCUS side-band FUN_80056208
Port
engine-battle-menus::battle_input, battle_flow, battle_tutorial, battle_stage_module; draw side engine-ui::battle_command_ui
Confidence
Confirmed - every edge is a constant store read from the disassembly; the D-pad map is capture-checked

The flow

Init 0x00FUN_801D84C0 Intro timer 0x0A / 0x0Bplates + formation banner Stage module 0x0CCort arrival owns the frame Round start 0x14one frame; only writer of 0x1E Begin / Run 0x1Eonce per round Run confirm 0x32cancel returns Right Command ring 0x28four D-pad arms, per member Left / confirm Auto / Command 0x78attack-mode prompt Left Item window 0x3Csub-cursors 0x5B, 0x5D, 0x64 Magic window 0x46sub-cursors 0x5C, 0x5E, 0x65, 0x67 Up Right Arts entry 0x50directional input Command Target cursor 0x5Aangular enemy ring next member Commit confirm 0x6Eafter the last member commits Down = Spirit, last member Round armed 0xFEctx[6]=0xFF, ctx[7]=0 Begin Action SMruns every queued action round over: 0x14, round index +1 run confirmed
The prompt at 0x1E belongs to the round, not the turn: 0x14 is its only way in, so every round opens with Begin / Run and each member then picks from the ring in turn. The item and magic windows commit through their own target sub-cursors and return to the ring for the next member, or raise 0x6E after the last. A back attack skips the prompt and arms the round straight from the intro timer.

State table

ctx[+0x06]ArmOn screenLeaves to
0x000x801D0DD0init0x0A
0x0A / 0x0B0x801D0DE0 / 0x801D0E3Cparty plates, formation banner, intro timer ctx[+0x6D6]0x14, or 0xFE on a back attack
0x140x801D0EC4one-frame round setup0x1E, unconditionally
0x1E0x801D102CBegin / Run prompt0x28, 0x32, 0x6E
0x280x801D1188command ring0x1E, 0x3C, 0x46, 0x50, 0x5A, 0x6E, 0x78
0x320x801D10F8run confirm0x1E, 0xFE
0x3C0x801D17DCitem window0x28, 0x5B, 0x5D, 0x64
0x460x801D19F8magic window0x28, 0x5C, 0x5E, 0x65, 0x67
0x500x801D1D84arts command entry0x28, 0x5A, 0x78
0x5A0x801D21CCtarget cursor0x28, 0x50, 0x6E, 0x78
0x640x801D2A00item window's target confirm0x28, 0x3C, 0x6E
0x6E0x801D3024all members committed - Begin / Reselect0x1E, 0x28, 0xFE
0x780x801D16E8Auto / Command prompt0x28, 0x50, 0x5A
0xFE0x801D31E8round armed0xFF (idle while the action SM runs)

The “leaves to” column is the exhaustive set of byte stores to ctx+6 inside each handler's address range, resolved by constant propagation. Two things in it are easy to get wrong:

  • The target sub-cursors are two disjoint runs, 0x5B..0x5E and 0x64..0x67. Nothing handles 0x5F..0x63.
  • 0xFF is not a case. Nothing tests for it; idle is reached by falling through to the default like any unlisted value. 0xFE is the real “round armed” case.

This byte and the action SM's ctx[+0x07] are cursors over the same struct and their value spaces collide: ctx[7] == 0x64 is the start of a party run, ctx[6] == 0x64 is a target confirm. Say which byte a state number belongs to.

Every surface is a D-pad map

The command UI is not one menu and not a scrolling list. It is three small clusters of plate chips, each around a D-pad glyph, and there is no face-button map: each chip sits on a D-pad arm and its handler tests the packed direction mask for that arm. Read against the raw BIOS pad word the masks look like face buttons, which is how the surface was once misread. A capture settles it: in the ring, one Up press opens the item window within three frames, while repeated Triangle presses do nothing.

StateArmChipGoes to
0x1ELeft, or confirmBeginthe ring (0x28); 0x6E when nobody can act
0x1ERightRun0x32
0x28UpItem0x3C
0x28LeftAttack0x78, 0x5A or 0x50, by the option word 0x800846C4
0x28RightRa-Seru magic0x46
0x28DownSpiritcommits; 0x6E on the last member
0x28cancel-0x1E on the round's first member, else the previous member's ring
0x78Left, or confirmAutotarget cursor 0x5A
0x78RightCommandarts entry 0x50

Attack is a door, not a strike. It opens the Auto / Command prompt, and the option word decides whether that prompt is shown (0), skipped to auto-target (1) or skipped to directional entry (2). With the Battle Command setting's highlight widget up, the same table reads as highlight-then-confirm: a pre-dispatch block walks the highlight and rewrites the input word to the highlighted arm's mask on confirm, so each handler's direction test doubles as “take the highlighted chip”.

The ring's cancel is its first test and forks on the step counter ctx[+0x1F] - how many members the cursor has walked past this round. On the first member it returns to Begin / Run; otherwise it steps back to the previous member's ring and discards that member's commit.

Commit log and confirm

Each committed member gets a row of three screen elements - name, command, target - from placement records 0x2B + 3n, where n is the step counter. The commit arms of the window-choreography helper FUN_801D388C land them, seat them off the measured width of the name (x = 16, name_w + 0x20, name_w + 0x60) and scroll earlier rows up as the party commits. An all-target commit logs a dedicated “all” record in the target column.

The commit-confirm screen (0x6E) is raised after the last member that can act has committed, for every party size - a solo party reaches it off its only member. No option gates it. Begin arms the round (0xFE); Reselect reopens the ring.

A sweep target reaches the action SM as a target-group code, not a sentinel: the flow writes actor +0x1DD = 8 for the party row and 9 for the enemy row, and a self-target writes the caster's own slot. The single-enemy cursor is not a slot-order walk either: FUN_801D8A88 builds a ring of the seated monsters and FUN_801D8D00 steps it, so Left and Right move to the angularly nearest live monster. The cursor opens on the member's last target while it stands, so a bare confirm strikes the same foe again. The ring covers the first four monster seats only: a fifth (a Divide clone's) can be targeted only once the four below it are down.

The sparring tutorial (overlay 967)

The “how to fight” boxes of the Tetsu sparring fight come from one stage overlay. Its hook FUN_801F6B70 is indexed by ctx[+0x06], and its nine live table slots are exactly the command-flow states above minus the magic window - the spar teaches attacks, items, Spirit and Hyper Arts, never magic. The hook table and every prompt string are resident in the overlay; the battle script, the MES text and the battle overlay carry none of them. The machine exists in exactly one place on the disc.

Retail has no party override for the spar: it is Vahn against Tetsu only because the story's party is Vahn alone at that point. The port makes that a rule and seats Vahn alone whenever the fight is the spar, whatever the field party holds. The fight ends on a countdown the hook arms in ctx[+0x6B4], not on a wipe.

The Cort stage modules (overlays 968 / 969)

Both modules are one function over the whole of their own code, and each is a phase machine on ctx[+0x289] driving the first monster seat. Neither is a cast module: none of the three cast entry tables names them, and only the stage pager brings them in.

  • 968 - arrival. Seven phases behind a jump table at the image base, run on a countdown word in the image's own data. It re-dresses the arena for the fight.
  • 969 - form change. The mid-fight transition to the evolved form.

The evolved-Cort fight is the one battle whose intro leaves the flow byte on 0x0C, a value the comparison tree has no arm for. The battle overlay never moves it off: the stage module does, from outside, handing back 0x0B when its arrival sequence is done. A port that does not run the module parks there forever.

See also

The full write-up - the pad-word construction, each commit arm's addresses, the string pools, the tutorial's prompt table and both boss modules phase by phase - is in docs/subsystems/battle-command-flow.md.