At a glance

Lives in
The field overlay (PROT 0897), the code the game streams into RAM whenever you walk around. The dispatcher FUN_801DE840 is one of the largest functions on the disc.
Scripts come from
Each scene's MAN, the script-and-data bundle shipped with the scene. Nothing about a town's behaviour is in the main executable.
Opcodes
43 opcodes 0x21-0x4F (gap 0x27-0x2A) over a byte stream, plus every byte 0x50-0x7F as a story-flag op. Bit 0x80 on any opcode aims it at another actor.
Dialogue
Has no opcode: talking resumes the NPC's own script, and its text sits inline in that script (below).
In the port
engine-vm::field (executing VM); legaia_scene_formats::field_disasm (side-effect-free disassembler, CLI asset field-disasm).
Confidence
Confirmed - disassembly of the overlay plus live captures (how we know).

What a script does in a scene

A scene's MAN holds four kinds of script, and each one is started by something different. Nothing schedules them centrally: each kind waits for its own trigger.

what starts it which MAN record what it drives Scene load,then every frame P1 record 0system script, context 0xFB music, camera zones,story gates, wall paints Touch oraction button P1 records 1..one placement per NPC or trigger NPC talk, chests,save points Contact or buttondoors open on contact P0 bind scriptsbound to a map object at load house doors,cupboards, props Op 0x44 or awalk-on tile P2 recordsspawned if their flag gates pass cutscenes, story beats,town exits
Four carriers, four triggers, one interpreter. A door is a script on a map object, not a scene change; a town exit is a partition-2 record that runs the named scene change (0x3F) when a walk-on tile spawns it.
  • NPCs. Each placement record seats an actor (model, idle clip, tile) and carries its script. A short spawn section runs at load (class flags, starting facing, flag-dependent relocation), and the rest waits for the player to touch it.
  • Doors and props. A partition-0 record bound to a map object plays the door's swing clip and, for an interior, teleports the player within the same scene grid (walking into a house).
  • Cutscenes. A partition-2 record is a timeline: it walks actors with cross-context motion ops, stages the camera, shows lines in the shared dialog box and sets the story flags that move the plot on. The narration crawl and camera mover it spawns are ordinary actors that run in parallel.
  • Gameplay hooks. Battles (3E FF row), minigames (3E with op0 ≥ 100), FMVs (4C E2), shops and the name-entry screen (0x49) are all script ops; the scene's script decides when each happens.

How a frame runs

A script runs only while its actor is engaged (bit 0x100 of the actor's flag word). While engaged it gets one slice per frame, and a slice is not one instruction: it runs instruction after instruction until something stops it.

Context engaged touch, spawn or system pass Run the opcode at PC bit 0x80: act on a target actor handler returns the new PC PC advanced: next op, same frame Wait 0x4A, flag spins, 0x49 armed resume: same op, next frame Motion park 0x37 / 0x41 / 0x47 set the halt bit resume: when the walk kernel lands Text byte < 0x20: the shared dialog box resume: when the box closes 0x21: end of pass engaged bit cleared, PC kept resume: at the next trigger
One slice. There is no scheduler and no instruction budget: every engaged script runs until one of the four stops on the right. A scene's whole entry prologue runs inside the load frame for this reason.
  • Every engaged script runs every frame. The frame driver walks five actor lists; a field actor with the engaged bit runs its slice. Scripts are not time-sliced against each other.
  • A script is a series of interactions. Each one runs from where the last 0x21 left the PC to the next executed 0x21. An NPC that says one thing on the first talk and another afterwards is one record whose branches all jump back to a shared flag selector.
  • Four things engage a script. The touch post (contact or the action button), the record spawner (a new partition-2 context), the system script's own driver, and another script raising the engaged bit on an actor with a cross-context flag set. A cutscene that merely walks a villager runs on its own slice against the villager's actor and never runs the villager's talk body (engagement).
  • The system script sits out other scenes. It starts a new pass only while no other script holds the player, and not at all while a battle is committed, so a town's per-frame loop pauses during an NPC talk or a cutscene.
  • A frame is a game frame, not a screen refresh. The frame loop runs once every two refreshes in a town or dungeon and once every three on the world map or in a heavy scene, and the scripts run once per pass. A trace of the running game counts every script step on a refresh where the frame loop ran, in seven scenes at both rates. The port used to step its scripts on every refresh, so a script's own loops ran two or three times as fast as retail's; it now steps them once per game frame, and only the dialog box still reads the pad on every refresh.
  • Waits are display frames. WAIT_FRAMES adds the frame-skip factor per visit, so "wait 30" is 30 display frames whatever the logic rate. A port has to pace the VM from a display-frame clock.

The scene's own script is the readable example. Rim Elm's starts a track, clears two story-flag banks and stops the track again 32 instructions later on a first visit, all before anything is drawn; the loop after it is a 20-instruction run of player box tests choosing the camera. Stepping one instruction per frame would play half a second of music retail never lets you hear.

Where scripts live

The carrier is the scene MAN (asset type 0x03). The scene loader installs each placement record's script pointer on its actor and starts the VM on the body that follows the record header.

PartitionRecord headerHolds
0[n][n×2 name][attr]Named objects; bound to map objects as touch scripts (doors, cupboards)
1[N][N×2 locals][model][anim][bx][bz]Record 0 = scene system script; records 1.. = one per NPC or trigger, script body after the header
2name + three condition blocksCutscene and beat records, spawned by op 0x44 or a walk-on tile when their flag gates pass

Thirteen scenes ship a second, plain MAN inside a streaming PROT entry (the "variant MAN"); for two dungeons it is the scene's only MAN, so a story-flag writer that seems to be missing is usually there. The prescript table that precedes some scenes (scene_event_scripts) is not field-VM bytecode: it holds move-VM stager records.

Placement header: model and idle pose

ByteMeaning
model < 0xF0Scene TMD index (pool slot model + 5; slots 0..4 are the party and the save point)
model >= 0xF0Global head slot model − 0xF0: Vahn, Noa, Gala, save point
animScene-bundle ANM record index + 1 (0 = none); party models resolve it against the locomotion bundle instead
bx, bzTile position; (127, 127) parks a conditional spawn a script places later

The header tile is not always where the actor stands. The spawn section runs at load, and a flag-chosen arm that moves the actor (op 4C 51, or a bare 0x23) relocates it before the first frame draws - Rim Elm stages its two running kids on the standing kids' exact tiles and dispatches them across town that way.

A spawn section can also end on a compass walk of its own actor (op 0x41). The walk halts the actor but the load slice runs on past it, and the walk kernel plays the half-tile leg out over the next few frames. Shop staff use it to stand on a tile corner the tile grid cannot name: Drake Castle's vendors (23 64 4D, 41 06 01) seat on their tile, then step half a tile along +X to stand behind their counter. A talk record's own step walks the talker the same way, and the conversation waits until it lands.

The load slice does not wait on its own walk either. The scene loader holds a spawn bracket word at 1 around the whole spawn loop and the object binds (0x8003B73C to 0x8003B928), and the dispatcher lets a spawn section's ops on its own walking actor through while it is up; the respawn op 4C D2 and the re-seat 4C C3 raise it the same way. The Water Gate Controller in suimon relies on it to hide its own figure (render scale 0, no drop shadow) right after the walk that seats it, so a retail visit shows no figure at the controller.

One record, two scripts, one cursor

A placement record is a single byte stream carrying two scripts back to back, and where the second begins is the thing ports get wrong.

headerlocals, model, seat spawn sectionruns once at scene load 21 interaction sectionops, 0x1F text, picker tables, branches 21 first opcode the cursor after scene load: a talk starts here branches jump back to a shared selector
The runner stores the forwarded PC before it tests for 0x21, so the cursor it leaves behind already points past the byte. An interaction therefore begins one instruction past the spawn terminator, never at the record's first opcode.
  • Entering at the first opcode is the classic mistake. It re-runs the spawn section on every talk - its seat pokes and any story flag it writes - then trips the terminator and falls through to the first text line, skipping the record's own prologue. For a minigame door, whose first text line is the refusal the failed coin check jumps to, that goes straight to “no tokens”.
  • A talk leaves the cursor where it ended. The next touch resumes from there, so a chest ends its talk on the grant, the flag and a 0x21 with its “The treasure chest is empty!” loop behind it, and a pickup spot such as Drake Castle’s Platinum Card ends on a silent loop: each grants once. Restarting every talk at the scan’s entry re-runs the grant. The entry is also the first 0x21 the spawn section executes, not the first in its bytes: a pickup spot’s flag test jumps over a terminator, and a scan that stops on it skips the flag, the item and the cue. The engine keeps the one cursor for the talk and for the return from a talk-staged battle, so Tetsu’s spar returns past its own terminator as retail’s does.
  • Nothing gates on the record containing text. A record whose interaction is camera work, an affordability check and a door warp runs exactly as a talk does. A minigame door is an ordinary member of the pipeline, not a special case.
  • Each partition has a different header. Reading a door record with the placement formula starts three bytes late, mid-instruction; the walk resyncs somewhere arbitrary and silently drops ops. That is the difference between a door that works and one that does not.
  • The record index is flat. Walk-on triggers and script-table teleports number records across all three partitions concatenated. Op 0x44 takes the same flat number and re-bases it into partition 2 itself.
  • Text and pickers are strides of the stream. A walk that treats 0x1F as an unknown opcode ends at the first line of every record, and everything behind it - picker jump tables, branch handlers, the shop - is never reached. The same boundary tells dialogue from coincidence: the bytes 1F 50 4B 00 inside an instruction's operands are not a text segment.

Engagement: who is allowed to run

A script context runs only while its engaged bit is up. The runner steps it, and the runner is also what ends it: when the instruction it just executed is a raw 0x21, it clears the bit and drops a count of running scripts; when that count reaches zero the player's own held bit clears. On every frame it steps anything, it raises that player bit - which is what stops the pad while a script runs.

  • A script can wake another actor. A cross-context flag set lands the engaged bit on the target, and the target's record resumes from its own cursor on the next frame. One boss's record stages its fight and parks; after the battle the scene's system script tests a flag and wakes him, and his record resumes at the scene that follows the fight.
  • The system script reaches props the same way. Once a wall is opened, one town's system script freezes two gate leaves on the first frame of their clips and parks the stone block the opening leaves behind - writes that land on the objects' actors, whose animation and draw read them the next frame.
  • A glide parks the talker. A mid-talk walk on the talking actor itself halts the record until the walk kernel's last frame releases it.
  • A talk reaches other actors by route. The port's talk runner keeps one stand-in context for the record, so each op that swaps onto another actor needs a route to land on it: the walk C7 <id>, the seat A3 <id> x z, the flag writes B1 / B2 <id> <bit> (bit 8 engages the target), and the reads CC <id> A0 / A1. The morph ops are the last route: CB <id> arms the target's lanes and AB / AC / AD <id> reach its envelope word, so a talk that waits on another actor's morph waits on that actor's envelope (field ambient FX). A talker naming its own id keeps its own context.
  • A talk that engages someone else has done its work. The raw 0x21 after it ends the talk with no first text box in its place, so a sleeper who ticks a counter and wakes a spirit says nothing himself; the spirit speaks, one block per engagement.
  • A spawned placement starts disengaged. The spawn pre-run clears the bit before it runs the spawn section, and the talk body after the terminator waits for a touch.
  • A spawned record ends at its first executed 0x21. A partition-2 context sits on a list the id resolver never walks, so no script can wake it again, and the bytes after the stop run only when a jump lands on them. Noa's “take me with you” on Mt. Rikuroa depends on it: the Yes answer plays the join and stops, and the No answer's brooding and second ask after it are reached only by the picker's jump.

A story flag can be a position test

A flag that is set in one place and tested in another is usually a progress latch. But the same ops build a continuously re-evaluated predicate when the scene's per-frame body owns them. One plaza's system script clears a flag twice during the load, then every other frame tests whether the player stands outside a box of tiles and sets it if so. Forcing the player inside stops the set on the next pass and nothing writes the flag again: its value is a property of where the party is standing. It is the gate on that scene's door records, so what it encodes is “the party is not in the plaza”.

Two consequences. A save state cannot be read as evidence of what an entry script ran, because the entry both clears and re-arms such a flag. And a port must re-seat the script's player anchor from the live player before every frame slice; seeding it once at scene load leaves it at the origin, outside every authored box, and every box test in every scene then answers “outside” for the whole visit.

Dispatch and targets

The interpreter is a plain function, new_pc = run(buffer, pc, ctx). Before its 43-way switch there is one prefix check: if the opcode's high bit is set, the next byte names the actor this instruction acts on, and the handler runs against that actor's context. A cutscene can say "the king walks to X" without becoming the king.

Target byteResolves to
0xF8The player
0xFBThe scene's system context (P1 record 0)
otherThe actor whose bind index matches; a halted target (flag 0x400, mid-walk) refuses the op until it lands, except the un-halt 32 0x400 and any op from the scene's system script, which lands on the walking actor at once

A scene's entry script seats the player this way. An inn that plays a dream sends the party to dream, which hands it back through a scene change whose arrival tile is the off-map hide tile; the destination's entry script reads the inn's return flag and runs A3 F8 <x> <z>, which lands on the player and puts the party on the inn bed (Ratayu's is A3 F8 F4 8B).

That refusal is how a cutscene choreographs its cast: it starts a walk on an NPC and runs on, and its next op on that NPC waits at the dispatcher until the walk lands. Only a walk aimed at the player parks the calling script as well. The context record's fields and every target rule are on internals.

The system script is the exception. The dispatcher checks the calling context, and the scene's own entry script is let through a halted target. In Conkram's past plaza (conc2) the light shaft over the clam's pedestal walks a step as it spawns, and the entry script parks it, with the clam, off the map outside the award ceremony. Refused, the shaft stayed standing over an empty pedestal.

Turning to face someone

The same acquire is how a cutscene turns heads. CC <id> 85 <budget> <bind> (and its 8E / 8F siblings) hands the target actor the walk kernel's face-target leg: it turns toward the bound actor (F8 = the player) over a frame budget, zero snapping at once, and the leg's last frame clears the halt. "Noa turns to Vahn" is CC <noa> 85 <budget> F8; cutscene records issue over a thousand of these on NPCs and as many again on the Noa / Gala placements. Aimed at the player, the op parks the calling record too, so a cutscene waits for Vahn's turn before it moves on.

Outside a cutscene - a scene's entry script, a conversation - a face-at aimed at another actor still turns that actor, and in a conversation the talker's own face-at holds the record until the turn lands. Two neighbours finish the picture: 4C 3A sets the player's heading to the arrival facing a door wrote from its direction byte (a card load and New Game zero it), and 4C 45 is a look turn - a rotation the animated renderer applies to one object of the rig, in every captured case the head, ahead of its keyframe pose. The port runs all of these on both play hosts.

The instruction set

Most of a town is built from a handful of these: flag tests, waits, walks, scene changes. Two opcodes, 0x43 and 0x4C, are sub-dispatchers hiding dozens of sub-ops each.

GroupOpcodesWhat scenes use them for
Flow21 26 40 42 4D 4EEnd of pass (21), relative jumps, inline data skips, branches on the held pad, on the player standing in a box, or on HP / gold / level / a random roll / a script counter
Timing4A 2D 30 33Wait N display frames; spin until a local, global or context bit flips - the "play this clip and wait for it" idiom
Flags2B-33, 5x 6x 7xThree small banks (per-script local, a transient global word, the actor's own flag word) and the persistent 4096-bit story-flag bank in the save block
Actors22 23 37 38 41 43 47 4BPlay a move clip, teleport to a tile, glide or walk to a tile, set facing, actor control (arc jumps, position tweens, shutter blackout, ending widgets), morph lanes
Camera45 46Stage, save, load and release camera shots; set the visible tile window
Sound35 36BGM start / pause / swap; XA voice clips, SFX cues and the side-band sound bank
Party and bag39 3A 3B 3C 3DGive an item (the chest op), add gold, set a bag slot, add or remove a party member
Scene flow3E 3F 44 49Start a scripted battle or a minigame, change scene by name, spawn a partition-2 record, open a menu-class screen (shop, save, name entry, code lock)
Effects34 4FColour fades, attached lights, scene-local 3D animations; scene register writes
Grab-bag4CSixteen sub-dispatchers: wall paint, camera focus, actor tint and clone, floor ladder, take item, coins, FMVs, timed flags

Every opcode with its byte width, the sub-op tables for 0x34 / 0x35 / 0x36 / 0x43 / 0x45 / 0x49 / 0x4E, and all sixteen 0x4C nibbles are on script VM internals.

Story flags

"Has the player beaten this boss?" lives in a 4096-bit array at RAM 0x80085758, inside the save block. The VM reaches it through the dispatcher's default arm: any byte whose high nibble is 5, 6 or 7 is a flag op, and the opcode's low nibble plus the next byte form the flag index (bits are MSB-first within each byte).

Lead byteEffectBytes
0x5xSet flag2
0x6xClear flag2
0x7xTest flag; when set, jump by the following 16-bit offset (counted from the end of the index byte), else fall through4

Partition-2 records gate on the same bank, so a flag set by one script is visible to the next record immediately. A beat that should play once self-latches: its script sets the very flag its gate tests. Some story-numbered flags are mechanism, not progress: the 0x527..0x531 scene-transition scratch band every entry script rewrites, the 0x00F door busy-mutex in Drake Castle, per-visit lift state. Read those as traffic. The disc-wide "who sets flag F" question is answered by legaia-engine man-scripts --system-flag-census; its caveats are on internals.

Field dialogue has no opcode

Press the action button in front of a villager and a text box opens, but there is no "show dialogue" instruction. Talking is a three-step pipeline over the NPC's own bytes.

  1. TriggerThe facing probe picks the actor in front of the player and the touch post engages its script. No opcode is involved.
  2. TextThe record carries two scripts back to back under one cursor: the spawn section, then the interaction. The interaction starts one byte past the spawn section's 0x21, and its lines are 0x1F-led glyph runs in the MES encoding.
  3. DisplayThe runner stops at the text byte and parks the context on the one shared dialog box; the pager shows it a box at a time. When the box closes, the script runs on.

The box has one owner at a time: a script that reaches text while another holds the box waits for it, while every script not on text keeps its slice. Op 0x3E is often mistaken for "talk to NPC"; with op0 < 100 it installs a scripted battle.

How we know

Function / dataAddressWhat it provesDump
The dispatcherFUN_801DE840The 43-way switch, prefix retarget, every handler and PC deltaoverlay_0897_801de840.txt
Frame driverFUN_8002519CFive actor lists at _DAT_8007C34C..6C, jalr node+0x0Cfuncs/8002519c.txt
Field actor tickFUN_8003BC08Flag-word routing: 0x100 script slice, 0x400 walk kernel, +0x80 motion VMfuncs/8003bc08.txt
Script sliceFUN_80039B7CRun-until-stop loop; 0x21 clears the engaged bit; the dialog-box parkfuncs/80039b7c.txt
System scriptFUN_8003AB2C / FUN_801DA51CRecord 0 run inline at load; per-frame passes gated on the player being freefuncs/8003ab2c.txt
Scene loaderFUN_8003A1E4Script pointer at actor+0x90, body at 1 + 2N + 4, spawn-section pre-runfuncs/8003a1e4.txt
Touch postFUN_801D5B5CEngages the touched actor unconditionally, text or notfield overlay dump
Target resolverFUN_8003C83C0xF8 player, 0xFB system, else bind indexfuncs/8003c83c.txt
Story-flag bankFUN_8003CE08 / CE34 / CE64Set / clear / test on DAT_80085758, MSB-first per bytefuncs/8003ce08.txt
Record spawnFUN_8003BDE0Partition-2 gate check for op 0x44 and walk-on tilesfuncs/8003bde0.txt

Source of record: docs/subsystems/script-vm.md and script-vm-menuctrl.md, which also cover the overlay's menu-support functions and the per-scene flag families.

See also