Battle command flow
Before any action runs, the player has to be asked what to do. This is that half of a battle: one state byte that walks the intro, the Begin / Run prompt, the D-pad command ring and its sub-screens, and hands a fully committed round to the action state machine. Three stage overlays hook the same byte: the sparring tutorial and the two Cort boss-stage modules.
At a glance
- Driver
FUN_801D0748in the battle overlay (PROT 0898, base0x801CE818)- State
- One byte,
ctx[+0x06]. Dispatch is a comparison tree at0x801D0C84..0x801D0DC8, not a jump table; 22 live cases, everything else falls to a default - Hand-off
- State
0xFEstoresctx[+0x06] = 0xFFandctx[+0x07] = 0: the action SM takes the frame - Hand-back
- The action SM's round end stores
ctx[+0x06] = 0x14and bumps the round indexctx[+0x28A] - Input
- The packed pad word: Left
0x8000, Right0x2000, Down0x4000, Up0x1000; confirm and cancel masks at0x800846D0/0x800846D4 - Stage overlays
- PROT 967 (tutorial hook
FUN_801F6B70), 968 (Cort arrival), 969 (Cort form change), all ticked by the SCUS side-bandFUN_80056208 - Port
engine-battle-menus::battle_input,battle_flow,battle_tutorial,battle_stage_module; draw sideengine-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
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] | Arm | On screen | Leaves to |
|---|---|---|---|
0x00 | 0x801D0DD0 | init | 0x0A |
0x0A / 0x0B | 0x801D0DE0 / 0x801D0E3C | party plates, formation banner, intro timer ctx[+0x6D6] | 0x14, or 0xFE on a back attack |
0x14 | 0x801D0EC4 | one-frame round setup | 0x1E, unconditionally |
0x1E | 0x801D102C | Begin / Run prompt | 0x28, 0x32, 0x6E |
0x28 | 0x801D1188 | command ring | 0x1E, 0x3C, 0x46, 0x50, 0x5A, 0x6E, 0x78 |
0x32 | 0x801D10F8 | run confirm | 0x1E, 0xFE |
0x3C | 0x801D17DC | item window | 0x28, 0x5B, 0x5D, 0x64 |
0x46 | 0x801D19F8 | magic window | 0x28, 0x5C, 0x5E, 0x65, 0x67 |
0x50 | 0x801D1D84 | arts command entry | 0x28, 0x5A, 0x78 |
0x5A | 0x801D21CC | target cursor | 0x28, 0x50, 0x6E, 0x78 |
0x64 | 0x801D2A00 | item window's target confirm | 0x28, 0x3C, 0x6E |
0x6E | 0x801D3024 | all members committed - Begin / Reselect | 0x1E, 0x28, 0xFE |
0x78 | 0x801D16E8 | Auto / Command prompt | 0x28, 0x50, 0x5A |
0xFE | 0x801D31E8 | round armed | 0xFF (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..0x5Eand0x64..0x67. Nothing handles0x5F..0x63. 0xFFis not a case. Nothing tests for it; idle is reached by falling through to the default like any unlisted value.0xFEis 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.
| State | Arm | Chip | Goes to |
|---|---|---|---|
0x1E | Left, or confirm | Begin | the ring (0x28); 0x6E when nobody can act |
0x1E | Right | Run | 0x32 |
0x28 | Up | Item | 0x3C |
0x28 | Left | Attack | 0x78, 0x5A or 0x50, by the option word 0x800846C4 |
0x28 | Right | Ra-Seru magic | 0x46 |
0x28 | Down | Spirit | commits; 0x6E on the last member |
0x28 | cancel | - | 0x1E on the round's first member, else the previous member's ring |
0x78 | Left, or confirm | Auto | target cursor 0x5A |
0x78 | Right | Command | arts 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.