At a glance

Dispatcher
FUN_801DE840(buffer, pc, ctx) in PROT 0897, returns the new PC. Dump ghidra/scripts/funcs/overlay_0897_801de840.txt.
Three exits
Advance (add the op's width), halt at PC (return the PC unchanged, so the op re-runs next frame - how every wait works), halt-acquire (freeze another actor by setting its halt bit and parking its resume pointer).
PC
A signed 16-bit halfword at ctx+0x9E: every relative jump is (base + delta) mod 0x10000, so 0xFFFE is "back 2".
In the port
engine-vm::field (step.rs per opcode), engine-vm::field::dispatch_leaves (the pure leaf helpers), legaia_scene_formats::field_disasm (widths for every opcode and every 0x4C nibble).

The context record

Each running script has a context, which for a placement is the actor record itself. Selected fields:

OffsetTypeMeaning
+0x10u32Flag word: 0x100 engaged (the script runs), 0x400 halted (mid-walk or halt-acquired), bits 0/1 collision- and touch-exempt (a door runs 31 00 as its swing starts), 0x20000 / 0x40000000 the contact class (button-gated props), 0x1000 animating
+0x14/16/18i16World X / Y / Z; a tile byte b becomes (b & 0x7F) × 0x80 + 0x40, plus 0x40 if the high bit is set
+0x26u16Heading (12-bit angle)
+0x50u16Script id / bind index (0xFB = system)
+0x54u16Wait accumulator: cleared by a motion park, ticked by WAIT_FRAMES
+0x5Ci16Move-table index (EXEC_MOVE)
+0x62u16Local flag bank, 16 bits; also the clip-control word whose bit 8 the clip tick latches at a clip's end
+0x72u16Render scale, 0x1000 = 1.0; 0 = invisible (trigger gizmos run 4C 40 00 00)
+0x74 / +0x78u32 / u16Draw word (tint colour + mode bits) and tint blend, written by 4C 81
+0x90u32Script buffer base
+0x94u32Resume pointer of a parked motion op (or a halt-acquire payload)
+0x9Cu8Dialog state: 1 waiting for the box, 2 owns the box
+0x9Ei16Program counter

The player context pointer is _DAT_8007C364; many opcodes branch on "is the context the player". The full field table is in script-vm.md.

Every opcode

OpMnemonicBytesWhat it does
21 24 25 48NOP1One shared handler. 0x21 also ends the slice and the interaction pass (the runner clears the engaged bit)
22EXEC_MOVE2Play move-table record id on the actor (99 = cancel); hands off to the move VM
23MOVE_TO3Teleport to tile (x, z); the player path also snaps the camera, the NPC path re-runs the visibility cull
26JMP_REL3Unconditional relative jump, pc + 1 + u16, 16-bit wrap
2B 2C 2DLFLAG set / clr / test2Local bank ctx+0x62. The test spins (holds the PC) while the bit is clear
2E 2F 30GFLAG set / clr / test2The 32-bit scratchpad word 0x1F800394: transient, not saved, partly engine state (bit 22 enables per-frame camera-zone re-query). The test spins while the bit is set, like 33
31 32 33CFLAG set / clr / test2The context flag word. 31 08 copies +0x26 to +0x5A instead of setting; 32 0x400 (un-halt) is the one op allowed through a halted target; 33 spins while the bit is set
34EFFECTvarSub-ops on op0 >> 4 (below)
35BGM4Eleven sub-ops (below)
36SOUND_CUE5XA voice clips, SFX cues and the side-band sound bank (below); the legacy SCENE_FADE label is wrong - every arm is sound
37 41GLIDE3Motion op: compass glide. Parks the script (halt bit, resume pointer at +0x94) and the walk kernel reads the op bytes in place each frame; 0x41 is half speed
38CAM_CFG (facing)3Set the actor's heading from the 8-direction compass at 0x80073F04, or halt-acquire it; despite the legacy name it is not a camera op
39GIVE_ITEM2Add one of item id - the treasure-chest path; the id is the inline byte, not a per-scene table (the chest's "There is a ..." text names the item separately)
3AADD_MONEY4Signed 24-bit gold delta, clamped to 0..9,999,999
3BSET_EQUIP_SLOT3Write one equipment-slot byte of a character record, then rebuild the derived stats
3C 3DPARTY_ADD / REMOVE2Sorted insert / remove, cap 4, leader fix-up
3EBATTLE / MINIGAME3+op0 == 0xFF or < 100: scripted battle on formation row op1; op0 ≥ 100: minigame door-warp (below)
3FSCENE_CHANGE7 + lenNamed warp [idx:i16][len][name][x][z][dir]: destination scene name, entry tile and arrival facing
40DATA_BLOCK2 + lenSkip len inline data bytes (payloads that other ops capture)
42COND_JMP5Mode 0 tests a bit of a global flag word; mode 1 the held pad (compass direction for op1 < 8, Circle / Cross / Square / Triangle for 8..11, always taken from 12). Taken = pc + 3 + u16, else skip 5
43ACTOR_CTRLvar22 sub-ops (below)
44SPAWN_RECORD2Spawn partition-2 record global_index as a new context if its flag gates pass (the opening scenes launch their timelines this way)
45CAMERAvarFour arms on op0 & 0xC0 (below)
46VIEW_WINDOW3 / 6The camera's visible-tile window at 0x1F8003E8..EB: long form 46 24 n0 n1 f0 f1 writes the four signed tile offsets, short form a half-width window. Not fog
47WALK_TO4Motion op: walk to tile at base step 4 << (b2 & 7), approach mode b2 >> 4
49STATE_RESUMEvarIdle / armed / done machine that opens a menu-class screen and waits for it (below)
4AWAIT_FRAMES2Add the frame-skip factor to ctx+0x54; halt at PC until it reaches the operand
4BANIMATE3 + 4nArm n VDF morph lanes (sub-entry, up / down ramp rates) and set the animating bit - the field sibling of move-VM op 0x0A, not a clip select
4CMENU_CTRLvarSixteen sub-dispatchers on the high nibble of op0 (below)
4DBBOX_TEST7Player inside the tile box: advance 7; outside: skip the trailing u16 less two, counted from the end of the instruction
4EINVENTORY_CMP7 / 9Compare-and-skip on party state (below)
4FSCENE_REGISTER_WRITE7Three u16 writes into the scene record at +0x10/+0x12/+0x14
5x 6x 7xSYSFLAG set / clr / test2 / 2 / 4The persistent story-flag bank (tour); the test jumps pc + 2 + u16 when the flag is set. 78..7F are tests too

No branch is absolute. Every conditional jump measures its operand from the instruction: 26 from after the opcode, 42 from three bytes in, the flag test from after its index byte, 4E from five bytes in at both its widths, and 4D and the 4C skip arms (A0..A2, 8B..8D, D1, DC / DE, E4, EB) through one shared exit that adds the halfword less two to a PC already past the op. A disc-wide test runs the port's interpreter over every shipped script and fails on any computed PC that lands between two instructions; the full table is in script-vm.md.

Any other masked opcode halts at PC. A ? (0x3F) inside message text is what makes a naive linear walk "see" scene changes in dialogue.

Sub-dispatchers

0x34 EFFECT

Sub (op0 >> 4)BytesEffect
07Effect-global colour and intensity (the fade pipeline)
113Attached light: a coloured rim on a target actor, extents in view-space units
22Re-script the attached light: when the actor has one and the next op is a 0x40 block, the light takes the block as its new script and the PC steps past it. No yield; with no light the block runs as an ordinary 0x40
34Spawn a scene-local 3D animation from the scene's prescript window at the actor
other-Halts at PC

0x35 BGM [35 lo hi sub]

SubEffect
1Start field BGM id (the loader resolves it asynchronously, ids)
2 / 3Stop (notes killed, cursor back to the start) / pause (notes keyed off, cursor kept)
4Replay: re-attach the slot and play the track again from its first event - no script word resumes mid-phrase (audio)
5Arm a timed sound-source auto-release after id vsyncs
6 / 7Flag set / target-sound-set
8Re-attach and re-apply the field volume
9Start behind a load barrier - how a cutscene changes music part-way
0xASwap-commit: wait for the load to settle, release the paused slot
0xBArm sub-op 9's barrier

0x36 SOUND_CUE [36 sel:i16 arg:i16]

selArm
0xFFFFWait on the load flag
bit 15 clear, non-zeroStart a CD-XA voice clip (arg >> 3, arg & 7)
bit 15 clear, zeroWait while the clip is playing
bit 15 set, sub 0 / 4Queue SFX cue arg / give the parked cue a delay
bit 15 set, sub 1 / 2 / 3Request, wait for and tear down the side-band sound bank

0x3E battles and minigames

Every op0 < 100 (and 0xFF) runs one body: re-derive the player's region (backdrop, Door of Light / Wind gates, world-map return point), point the system context at formation row op1, re-roll the step counter and request battle mode. 3E 00 02 installs row 2 exactly as 3E FF 02 would. op0 ≥ 100 enters minigame op0 − 100 through mode 24, which backs up the current scene name and restores it on exit:

Sub-idOverlayMinigame
0PROT 0972Fishing
1 / 20973 / 0974Dev modules OTHER2 / OTHER3
30975Casino slot machine
40976Baka Fighter
50977Monster-roster arena hub
60980Noa's dance

0x43 ACTOR_CTRL

SubBytesEffect
0 1 A BvarScripted arc jump (halt-acquire, target tile, floor lookup)
28Three-actor talk
3..610Camera-register zone ramp: while the player is in a tile box, lerp pitch / yaw / depth / projection on his Z. Every disc site is sub-3, a field-of-view ramp
7 / 817 / 2Spin-and-shrink deformation / reset: writes a row of the break-up table and ramps the actor's word, which hands its draw to the shatter renderer
910Position: immediate, or an eased move over ticks
C5Shutter blackout: two black bars close, hold and open
D / F6Screen frame: a w-wide opaque band round the screen in one colour, bevelled at the inner corners
E2Retire the first live screen frame
10..1521 / 12 / 14 / 14 / 10 / 14Screen widgets of the ending scenes: sprite, iris mask, VRAM rect copy, image panel, panel move / scale, letterbox
16+-Halts at PC

0x45 CAMERA

op0 & 0xC0ArmBytes
0x00Configure: write the staged camera fields named by a mask, optionally glide5 + 2 × set bits
0x40Load a saved shot20
0x80Save the current shot2
0xC0Apply: compose the follow shot for the player and commit it (a release, not a jump - its s16 is a glide length)4

A record that issues no 0x45 leaves the field camera's follow shot alone; a scripted camera only owns the frame once some beat has staged a shot.

0x49 STATE_RESUME

Idle: spawn the subsystem actor and park the operand pointer. Armed: return the same PC every frame. Done (the screen wrote 1): clear and advance. The sub-op picks the screen:

Sub-opScreen
0No screen of its own; carries the town-shop stock record 49 00 00 [count][ids..][name\0]. Done reads the count as the payload length and walks the name, so the talk resumes on the line after the record
1, 7, DNo screen (the save point's 49 01 opens the menu session)
2Code lock (five-symbol code in the operands)
3 nName entry for party slot n (below)
5Tile-board walk
6Casino coin counter
8 / 9Start / confirm menu; a prompt
BIncense wear-off notice (reached from the walk tick, not a script)
CReturn-to-title soft reset (the ending)

Before any screen runs, the subsystem actor's installer (FUN_801F1278) stands the player on the party leader’s idle clip, and takes down a dialog box a 0x24 continuation left on screen: it rewinds the pager’s state word DAT_801F2734 from its row-keeping idle 4 to 1, which draws nothing.

0x4E INVENTORY_CMP

Sub-opCompares
0 / 1A character's HP / MP against a fraction of its max
2Level
3Gold against a u16 - the inn's price gate
4A random byte (chance branch)
5..8Script counter slot sub − 5 (written by 4C CA / CB / CC)
9Casino coins against a u16
10 / 11Gold / coins against a u32 (9 bytes)

BGM ids

A script's "play track 2016" is an offset into one bank, not a table lookup.

IdResolves to
>= 2000The global music_01 bank in sound-test order: 2000 + i = sound-test track i (names)
< 2000Not a scene-block entry: the resolver uses the block index only as its change test and loads global slot 2 (the track 2002 plays). Retail stages no scene bank (audio)

The name-entry screen (49 03 n)

The field overlay's state machine runs the screen (renderer FUN_801E6B34, cursor cell _DAT_8007BB88, state _DAT_8007BB94: 1 editing, 4 confirm) and writes the typed name live into the character record at +0x2A7. Geometry in 320×240 pixels:

ElementGeometry
Grid window / name window(24, 91, 272, 120) / (196, 71, 88, 28), pause-menu skin
Charset7 rows × 17; glyphs from base + (4, 4), 15 px column pitch, 14 px row pitch
Working name(208, 79); teal caret 6 px after, 75%-duty blink
Control bary = 191: BS, the default name (restores the template; there is no space key), Select
Prompts"Select your name." at (176, 32); the confirm prompt at (172, 24), cursor opens on No

Reading the dump

  • Label-calls. Ghidra renders the shared epilogue as a call. In the assembly each arm exits with addiu s8, s8, N in the delay slot of j 0x801df09c, and that N is the op's PC advance. Labels promoted to fake functions: 0x801df098 (+2), 0x801df09c (+0), 0x801df8dc (+6), 0x801e00b8 (+3), 0x801e212c (+7), 0x801e3614 (BBOX outside), 0x801e3620 (+4). Grep the address in the dump before treating it as a callee.
  • Delay slots hide advances. The motion ops advance past themselves for every target (li s7, 4 in a delay slot); only a player target also parks the caller. The decompiled C hides this.
  • 16-bit jumps. [21] [26 FE FF] is the park-here idiom. A port that adds the delta in a wider integer turns every backward jump into a forward overrun.
  • Pure helpers. packet_length (FUN_8003CA38), party_flag_test (FUN_8003CE64), small_table_search (FUN_80042EE0), the little-endian loaders (FUN_8003CE9C / CEB8 / CED8) and the inlined tile_center are ported as functions in engine-vm::field::dispatch_leaves.

Flag census: when a row lies

The census walks bytecode linearly, and four things mislead it. A marked row means "check the record disassembly", never "discard".

  • Text aliases. Dialogue bytes land on the flag ops: S..W read as Set, a..g as Clear, q..w as Test. Rows carry clean and text_alias marks; a non-printable operand cannot be minted by prose.
  • Width blindness. A wrong sub-op width desyncs the walk and hides a real site behind a phantom; every 0x4C nibble decodes with the executing VM's widths.
  • Variant carriers. The census walks the bundle MAN and the streaming variant MANs (--variant <entry>); --p2-gates prints the partition-2 spawn conditions.
  • Developer flag menus. Several shipped records carry a debug picker whose options write the scene's flags (nine bundle MANs label theirs as flag menus, Jeremi's inn among them, and the streaming carriers hold more). Their rows are clean and real, and mean "an option nobody picks in retail play"; they carry debug_menu.

FMV triggers and the disassembler

Only two code paths start a movie: the field op 4C E2 (its u16 operand is the FMV id) and the title attract loop (movie 0). Eight scenes carry a 4C E2: town01, garmel, deroa, chitei2, dohaty, town0d, uru, jouine; the operands decode statically from each MAN (cutscenes).

asset field-disasm file <PATH>
asset field-disasm scan-prot --prot <PROT.DAT> --cdname <CDNAME.TXT> --bytewise

The walker mirrors the executing VM's widths without side effects, so any byte buffer is safe input. A 4C E2 hit inside a move-VM prescript record is a false positive.

Readings this page replaced

  • The prescript table was read as field-VM bytecode; it is move-VM stager records.
  • Op 0x39 was labelled PLAY_SFX; it is the item give. Op 0x36 was labelled SCENE_FADE; every arm is sound. Op 0x3E with op0 < 100 was read as an NPC-interaction opener; it is the scripted-battle install.
  • 4C 30 was read as an input lock; it raises the ambient-particle gate. Nibble 9 was read as a fade family; it is the floor-height ladder.
  • The story-flag base was mislabelled DAT_80086D70 by double-counting the save-block displacement.

More on do-not-re-walk.

See also