How it works

If the actor VM animates sprites, the field VM scripts the world. Every NPC has a small bytecode program that says "walk to this corner, wait, walk back, when the player says hi say something". Every cutscene is a longer bytecode program that says "fade in, position the camera, play this animation, open this dialog box, wait for confirm, fade out, change scene". The field VM is what runs those programs.

The mental model: each running script has its own context struct (passed around as ctx_ptr). The context holds the script's current position in the world, its program counter, a 16-bit local flag bank, a halt flag, and a few timer slots. The dispatcher reads the opcode at the current PC and runs a handler that mutates the context (or the world). If a handler decides "I'm done for this frame", it returns the new PC and the next frame's tick comes back in.

One feature that distinguishes Legaia's field VM from typical event-script systems: cross-context targeting. The high bit of any opcode means "this instruction targets a different script context, identified by the next byte". The original script's context is preserved; the dispatch operates on the resolved one. So a cutscene script can issue "the king walks to position X" with the king's script ID as the target, without becoming the king. There are also two reserved system channels (0xF8 and 0xFB) for engine-wide effects.

Three of Legaia's flag-storage banks are exposed by clean opcode triplets: per-script local flags, global story flags (in PSX scratchpad), and ctx flag word. A fourth flag bank is hidden in the dispatcher's "default route" - opcodes whose high nibble is 0x5, 0x6, or 0x7 get dispatched to set / clear / test handlers operating on a 256-bit bitfield, almost certainly an "engine system flags" bank.

The decompiled source is at ghidra/scripts/funcs/overlay_0897_801de840.txt. References below to func_0x80xxxxxx are calls into SCUS_942.54; FUN_801xxxxx are sister functions inside the same 0897 overlay.

For the full opcode reference, the source markdown has the complete tables. This page summarises the structure; jump to docs/subsystems/script-vm.md for the definitive listing.

On-disc form: the scene MAN - not scene_event_scripts

The on-disc carrier for field-VM bytecode is the scene MAN sub-asset (asset type 0x03, the third descriptor in each scene's asset-table bundle; see man-relocation and legaia_asset::man_section). FUN_8003A1E4 walks the MAN's partition-1 actor-placement records, installs each actor's script pointer at actor[+0x90], and runs the field VM (FUN_801DE840) on the body 1 + N*2 + 4 bytes into the record. Partition-1 record 0 is the scene-entry system script; records 1.. are per-actor interaction scripts. These decode cleanly as field-VM (~8% linear-walk error on the retail town MANs).

The scene_event_scripts / scene_v12_table prescript is a DIFFERENT structure - not field-VM bytecode. The [u16 count][u16 offsets[count]] prescript (offset 0, or +0x800 behind the v12 header) was long assumed to carry field-VM scripts because its records open with 0xFFFF 0x0000. It does not: running the field-VM disassembler over them yields a 65–88% decode-error rate, the bytes are 16-bit word-aligned (low byte = opcode, high byte 0 on ~83% of body words), records terminate with a 0x0008 word, and the opcodes sit mostly below the field VM's 0x22 opcode floor. The 0xFFFF 0x0000 lead is a per-record header sentinel, not a frame-divider opcode, and record 0 is a fixed 768-byte dispatch table. The consuming command VM is not yet identified. See legaia_asset::scene_event_scripts (record_words) and the disc-gated scene_event_records_word_aligned_real test. The engine never feeds these prescript bytes to its field VM; the vestigial Scene::find_event_scripts() / World::load_field_record() diagnostic path that does is why ticking a prescript record as field-VM "halts at pc 0 / yields immediately" rather than running scene logic.

Function signature

int FUN_801DE840(int buffer_base, int pc_offset, int ctx_ptr);
  • buffer_base - bytecode buffer base address.
  • pc_offset - current program counter, byte offset into the buffer. The function returns the new PC offset.
  • ctx_ptr - script execution context (see below).

The VM is not a step-and-yield loop - each call executes from pc_offset until something forces a return (instruction halt, branch back, target script done). The host calls back at the next frame (or external event) with the returned PC.

Top-level dispatch

byte *pc = (byte*)(buffer_base + pc_offset);
byte *ops = pc + 1;

// Extended/script-target prefix
if (*pc & 0x80) {
    // Switch context to script targeted by pc[1]
    if (pc[1] != ctx_ptr[+0x50]) {
        ctx_ptr = func_0x8003C83C(pc[1]); // resolve by ID
        if (ctx_ptr == 0) return pc_offset + 1;
    }
    if (ctx_ptr[+0x10] & 0x400 && /* not opcode 0x32 with bit 0x400 */) {
        return pc_offset; // halted, don't dispatch
    }
    ops = pc + 2;
    pc_offset += 1;
}

switch (*pc & 0x7f) {
    // 43 unique opcodes 0x21-0x4F (with gaps at 0x27-0x2A)
}

The high bit of an opcode means "this instruction targets a different script context". func_0x8003C83C resolves a script ID to a context pointer, with two reserved IDs:

IDResolution
0xF8Returns the cached pointer at _DAT_8007C364 (player context)
0xFB"System" channel - walks the linked list at _DAT_8007C34C for the entry whose +0xC slot holds 0x801DA51C
otherwiseRegular script-table index

Context struct (selected fields)

Per-script state, passed as ctx_ptr. The full table is in the source markdown; the most-cited fields:

OffsetTypeMeaning
+0x10u32Flag word. Bit 0x400 = "halted". Multiple bits gate per-opcode behaviour.
+0x14/16/18u16World X / Y / Z (in 0.5-tile units, formula (b & 0x7F) * 0x80 + 0x40).
+0x50u16Script ID. 0xFB = "system" channel.
+0x54u16Wait/timer accumulator. Cleared by YIELD; ticked by WAIT_FRAMES.
+0x5C / +0x5Eu16/i16Move-table index, sentinel set by op 0x22.
+0x62u16Local flag bank (16 bits). Manipulated by ops 0x2B / 0x2C / 0x2D.
+0x94u32Saved PC (set by YIELD; the dispatcher reads this on resume).

_DAT_8007C364 is the player context pointer - many opcodes branch on ctx_ptr == _DAT_8007C364 to switch behaviour. _DAT_801C6EA4 is the current world/scene pointer.

Opcode groups

The 43 opcodes cluster into themed groups:

Shared NOPs - 0x21 / 0x24 / 0x25 / 0x48

Four distinct opcode bytes share one handler that just advances PC by 1. Likely reserved/historical.

Action & control flow - 0x220x26

OpMnemonicEffect
0x22EXEC_MOVESchedule move-table playback. Calls FUN_800204F8 - the move-table consumer that crates/mdt targets.
0x23MOVE_TOTeleport ctx to grid position. Player path also calls func_0x80017EC8 (camera/scroll). NPC path sets facing and calls movement init.
0x26JMP_RELUnconditional relative jump.

Flag-manipulation triplets - 0x2B0x33

The cleanest group - three separate 1-bit-flag banks each with set / clear / test+skip:

OpMnemonicBank
0x2B / 2C / 2DLFLAG SET/CLR/TSTPer-script local flags at ctx[+0x62] (16 bits). Sub-routine state, conditional dialog branches.
0x2E / 2F / 30GFLAG SET/CLR/TSTGlobal story flags at _DAT_1F800394 (32 bits, in PSX scratchpad). Persistent across script runs.
0x31 / 32 / 33CFLAG SET/CLR/TSTCtx flag word at ctx[+0x10] (32 bits). Halt state, move-chain state, render-gate state.

Effects, music, scene transitions - 0x340x36

  • 0x34 EFFECT - nibble-dispatched (sub-0/1/2/3): colour + intensity setup, effect/sprite spawn, actor-pool capture-and-yield, 3D animation playback via func_0x800252EC.
  • 0x35 BGM - sub-dispatcher with 11 sub-ops (start, pause, resume, stop, volume, etc.).
  • 0x36 SCENE_FADE - reads two 16-bit operands. 0xFFFF = wait for load flag.

Yield, sound, RPG state, dialog - 0x370x42

OpMnemonicEffect
0x37 / 41 / 47YIELD familySave PC, clear timer, set HALT flag. Player ctx propagates the halt to the caller.
0x38CAM_CFGIf op1 & 0x7F == 0: copies *(short*)(0x80073F04 + (op0 & 0xF) * 2) into ctx[+0x26]. Else: halt-acquire path - same predicate as 0x43 sub-0/1/A/B; on success set HALT, save PC, mirror to caller when ctx is the player, yield with resume_pc = pc + 3.
0x39GIVE_ITEMAdds one of inline item item_id to the inventory: func_0x8004313C() (inventory-window setup) then func_0x800421D4(item_id, 1) (add-item-by-id). PC += 2. This is the treasure-chest item-give path - the id is a single inline operand byte, not a table. (The earlier PLAY_SFX label was wrong: FUN_800421D4 is the inventory adder.)
0x3AADD_MONEY24-bit signed delta, clamped to [0, 9999999].
0x3BSET_ITEM_COUNTSet inventory entry. Inventory pages of 0x414 bytes.
0x3C / 3DPARTY_ADD / PARTY_REMOVECaps at 4 members. Updates leader. Refreshes display.
0x3EWARP / INTERACTField interact (op0 == 0xFF or op0 < 100) or minigame door-warp (op0 >= 100): sub_id = op0 - 100 selects a mode-24 minigame overlay (FUN_80025980 backs up the active scene name, streams the overlay via FUN_8003EBE4(sub_id + 0x4D) → extraction PROT 0972..0977/0980 - fishing 0, slot machine 3 (0975), Baka Fighter 4 (0976), dance 6 (0980) - and FUN_80026018 restores the scene + commits winnings on exit). The op carries no destination name.
0x3FSCENE_CHANGE (named warp)Named scene-change, NOT dialog. Copies a length-prefixed destination scene NAME from the bytecode ([i16 index][u8 len][name][entry_x][entry_z][dir]) and calls func_0x8001FD44 - the scene-change packet (writes the name to 0x8007050C/0x80084548; sets transition flag _DAT_1F800394 |= 0x40) - then sets the entry tile. This only looks like dialog when the walk desyncs on a literal ? (0x3F) in text; field dialogue has no dedicated opcode (it's the actor's inline interaction-script MES, triggered by the field-interact op 0x3E op0<100 + the actor-dialog SM FUN_80039b7c / pager FUN_801D84D0).
0x42COND_JMPMulti-mode conditional. Tests global story flag bank or screen-mode against an 8-entry table.

ACTOR_CTRL family - 0x43

22+ sub-ops, keyed on operand byte 0. Includes a halt-acquire dispatcher (sub-0/1/A/B), actor / sound / face / position cluster (sub-2 through sub-F), and an emitter setup family (sub-0x10 through sub-0x15) that dispatches into the FUN_801F8xxx particle/emitter cluster.

Counter / camera / render / state / move-block - 0x440x4F

OpMnemonicNotes
0x44COUNTERPer-frame counter / score / hit-counter tick.
0x45CAMERASub-dispatch on op0 & 0xC0: configure / LOAD / SAVE / APPLY.
0x46RENDER_CFGFog/render params.
0x49STATE_RESUMEMulti-frame state machine: tristate (Idle / Armed / Done). Done-state sub-0 walks an inline MES-shape payload (counts bytes > 0x1E with one-byte peek-extension for 0xCx prefix bytes) and advances PC by 5 + length + walked.
0x4AWAIT_FRAMESFrame timer; ticks ctx[+0x54].
0x4BANIMATEMulti-keyframe setup. Sets +0x10 bit 0x1000 (animation flag).
0x4CMENU_CTRLOuter-nibble-dispatched (16 sub-dispatchers). The biggest single opcode.
0x4DBBOX_TESTInside-box advances PC by 7; outside-box jumps via FUN_801E3614.
0x4EINVENTORY_CMPCompare-and-jump across page-banked inventory state and party-money/XP banks.
0x4FSCENE_REGISTER_WRITEWrites three u16 values to _DAT_801C6EA4 + 0x10/0x12/0x14.

The fourth flag bank (default-route)

The default arm of the dispatcher checks *pc & 0x70:

High nibbleSCUS dispatcherEffect
0x5xfunc_0x8003CE08SET bit
0x6xfunc_0x8003CE34CLEAR bit
0x7xfunc_0x8003CE64TEST bit (returns 0xFF if set)

All three operate on the same bitfield array based at 0x80085758. Each does index >> 3 to pick the byte and 0x80 >> (index & 7) to pick the bit.

// 0x8003CE08 (SET):
(&DAT_80085758)[(int)idx >> 3] |= (byte)(0x80 >> (idx & 7));
// 0x8003CE34 (CLEAR):
(&DAT_80085758)[(int)idx >> 3] &= ~(byte)(0x80 >> (idx & 7));
// 0x8003CE64 (TEST):
return ((&DAT_80085758)[(int)idx >> 3] & (0x80 >> (idx & 7))) ? 0xFF : 0;

The low 4 bits of the opcode plus the next operand byte form an 8-bit flag index, but with the “extended” prefix bit (0x80) preserved into the high bits the addressable space is 12-bit, suggesting per-script-context banks within the same array. The TEST dispatcher consumes two extra operand bytes (pc[2..4]) as the post-test action target when the bit is set.

An earlier draft mislabelled the base as DAT_80086D70 by double-counting the 0x1618 displacement onto 0x80085758. The Ghidra symbol DAT_80085758 is itself 0x80084140 + 0x1618, and the array is indexed directly from there - no further +0x1618.

This is a fourth flag bank, distinct from the three exposed by the explicit 0x2B0x33 opcodes - likely "system" / engine-wide event flags. It is not a wholly separate region: base 0x80085758 falls inside the story-flag RAM window 0x80085600..0x80085800 (at +0x158) and the bank extends past 0x80085800 (indices up to ~0xFFF reach 0x80085758 + 0x1FF). In a retail SC save block the bank lives at SC offset 0x1618, overlapping the story-flag bitmap (SC 0x14C0, 512 bytes) and continuing to the inventory array (SC 0x1818); seeding World::system_flags from sc_block[0x1618..0x1818] reproduces the live bank as of the save. Note this bank is not sufficient on its own to drive a scene's collision: the 0x4C nibble-7 wall paints reached through it are story-conditional collision deltas, not the base walkable grid (see field locomotion). Effective opcode space therefore includes the explicit 0x210x4F range and any byte whose high nibble is 0x5, 0x6, or 0x7 (potentially 192 more "wide" opcodes routed to three SCUS dispatchers).

BGM lookup table

There isn't really a "BGM → file" lookup table. The BGM ID is a PROT-relative offset. From FUN_800243F0:

if (_DAT_8007BAC8 < 2000) {
    _DAT_8007BAB8 = _DAT_80084540 + 6;            // scene-local: current scene PROT base + 6
} else {
    _DAT_8007BAB8 = _DAT_8007BC64 - 2000;          // global pool: separate base
}
_DAT_8007BAB8 = _DAT_8007BAC8 + _DAT_8007BAB8;     // final PROT index
  • bgm_id < 2000 - scene-local. Different scenes have different BGM at the same script ID.
  • bgm_id ≥ 2000 - global. Shared across scenes (cutscene / title / event music).

The "table" is the CDNAME.TXT name map's per-scene block layout. There's no separate BGM index in SCUS_942.54.

Helper functions

A growing set of small leaf helpers in the dispatcher's call graph are pure arithmetic - no globals, no overlay calls - so they get clean-room ports in crates/engine-vm/src/field_helpers.rs rather than host hooks, and the dispatcher arms call into them directly.

HelperOriginalUsed by
packet_lengthFUN_8003CA380x4C nE sub-1, 0x49
party_flag_testFUN_8003CE640x4C nC sub-1 (host-side)
small_table_searchFUN_80042EE00x4C nD sub-C/E
load_u16_le / load_u24_le / load_u32_leFUN_8003CE9C / CEB8 / CED8LE immediate decoding across many 0x4C sub-ops
tile_centerinline (multi-arm)0x4C nE sub-3/4, MOVE_TO, dialog spawn
  • packet_length(buf) measures one variable-length packet of the in-game text encoding: walks until any byte ≤ 0x1E (terminator), counts bytes ≥ 0x1F as 1 each, and bytes whose top nibble is 0xC consume the next byte unconditionally (escape, counts as 2). The terminator is not included.
  • party_flag_test(idx, flags) reads bit idx of a packed bit array, MSB-first per byte; returns 0xFF when set, 0 otherwise. Exposed to 0x4C nC sub-5/6 via the op4c_n_c_party_flag_test host hook.
  • small_table_search(needle, table, lo, hi) searches table[i*2] (stride 2, low byte of each short) for needle across [lo, hi); returns the index or SEARCH_NOT_FOUND (0x100) on miss.
  • The LE byte-load family assembles results from sequential bytes and returns 0 for missing bytes; the 24-bit version pairs with sign_extend_24 for the few opcodes (notably 0x4C nE sub-5's XP-add) needing a signed 24-bit immediate.
  • tile_center(b) is the grid-byte → world-coord conversion: b == 0 returns 0; otherwise (b & 0x7F) << 7 | 0x40, plus 0x40 if the high bit is set. The original inlines this in nine separate dispatcher arms.

The Rust ports are exhaustively tested alongside the helpers in field_helpers.rs.

0x4C nibble-D sub-4 / sub-5 - VRAM STP-bit set/clear

The 6-byte [4C, 0xD4|0xD5, x_lo, x_hi, y_lo, y_hi] operand is a (vram_x, vram_y) pair; the rect is hard-coded to w = 0x10, h = 1. The original runs the PsyQ libgs StoreImage → per-pixel STP-bit edit → LoadImage read-modify-write over 16 u16 pixels: sub-4 (op 0xD4) sets STP on non-zero pixels, sub-5 (op 0xD5) clears STP unless the pixel is already STP-only. The 16-element buffer lives on the dispatcher stack, not in the bytecode - it's pixels read from VRAM at runtime. The host hooks op4c_n_d_sub_4_vram_stp_set(x, y) / op4c_n_d_sub_5_vram_stp_clear(x, y) receive only the rect origin; a clean-room renderer that maintains its own framebuffer can emulate the read-modify-write itself.

Connection to other crates

  • crates/mdt - opcode 0x22 EXEC_MOVE drives the move-table consumer at FUN_800204F8. See Move-table VM.
  • crates/mes - field dialogue has no dedicated opcode: it is the actor's inline interaction-script MES text, shown by the actor-dialog SM FUN_80039b7c + pager FUN_801D84D0, triggered by the field-interact op (0x3E op0<100). The text crates/mes parses is that inline 0x1F/glyph stream. (0x3F is the named scene-change - func_0x8001FD44 is the scene-change packet, not a dialog opener.)
  • crates/anm - opcode 0x34 sub-op 3 plays 3D animations via func_0x800252EC.
  • crates/engine-vm - the clean-room Rust port at crates/engine-vm/src/field.rs. Reuses the same Host trait pattern as the actor VM.

Decompile quirks worth knowing

If you read the function dump and something doesn't add up, these are the usual suspects:

  • switchD_801e00f4::default() is misleading. Ghidra renders the function-epilogue tail block as a synthetic function call; in the original asm, opcodes that "fall through to default" actually advance param_2 via the addiu s8, s8, N instruction in the MIPS branch-delay slot of the j 0x801df09c jump. So 0x39, 0x3B, 0x44, 0x4C and friends DO advance the PC - just not in a way the C-level decompile makes obvious.
  • LAB_801df09c is just j 0x801e3628; move v0, s8 - return s8 unchanged. Most callsites jump there with an addiu s8, s8, N in the delay slot of the j, supplying the per-callsite PC delta.
  • 0x42 mode 0 jump-take target is pc + 3 + LE_u16(operand[2..4]) (non-extended), found via the join point LAB_801e35fc.
  • Relative-jump deltas wrap at 16 bits. Each script's PC is stored as a signed 16-bit value (*(short*)(ctx + 0x9e)), so every relative branch (0x26 JMP_REL, the 0x7x flag-TEST conditional jump, 0x42 COND_JMP, the 0x4E compare jumps) computes (base + delta) mod 0x10000. A delta with the high bit set is a backward jump - e.g. 0xFFFE = -2, the per-frame "park here" wait-loop idiom ([21] [26 FE FF] ping-pongs two bytes until a story flag flips a guarded TEST). Computing base + delta in a wider int without the 16-bit truncation turns every backward jump into a +0xFFxx forward overrun. The clean-room port models this with a rel_jump(base, lo, hi) helper that wraps in u16.
  • Intra-function label catalogue. Several iVar = FUN_801xxxxx(); return iVar; patterns in the C decompile look like helper calls but are intra-function j targets Ghidra promoted to fake function names - each is a addiu s8, s8, N; j epilogue block supplying a PC delta. Notable entries: 0x801df098 (PC += 2), 0x801df09c (PC unchanged, the epilogue), 0x801df8dc (PC += 6), 0x801e00b8 (PC += 3), 0x801e212c (PC += 7), 0x801e3614 (BBOX outside-box, pc + 5 + skip), 0x801e3620 (PC += 4). Always cross-check grep -n "0x<addr>" overlay_0897_801de840.txt before treating a FUN_xxxxxxxx reference as a separate function - the misleadingly-named dump file overlay_0897_801e3620.txt actually has entry 0x801e3578.

For the exhaustive opcode reference and the full 0x4C outer-nibble dispatcher table, see docs/subsystems/script-vm.md on GitHub.

Disassembler tool: field-disasm

crates/engine-vm/src/bin/field_disasm.rs walks a field-VM bytecode buffer and prints one mnemonic per encoded instruction. The decoder mirrors the width logic of step() without executing host calls or mutating ctx state, so it's safe to point at any byte buffer - it stays linear, recovers from unknown sub-ops one byte at a time, and never follows jumps.

# Walk a raw script body, print each opcode + operand:
cargo run -p legaia-engine-vm --bin field-disasm -- file <PATH>

# Detect a [u16 count][u16 offsets[count]] prescript at the start of <PATH>
# and walk every record body individually:
cargo run -p legaia-engine-vm --bin field-disasm -- scene-event-scripts <PATH> [--summary]

# Walk every PROT.DAT entry and report 0x4C 0xE2 byte-pattern hits with
# their CDNAME label and decoded fmv_id (filtered to the retail valid
# range 0..=8 unless --no-filter is passed; the runtime FMV-state table
# at 0x801D0A6C carries 12 slots - slots 5..=11 point at cut paths):
cargo run -p legaia-engine-vm --bin field-disasm -- scan-prot \
    --disc <PROT.DAT> --cdname <CDNAME.TXT> --bytewise

The library exposes legaia_engine_vm::field_disasm::{decode, LinearWalker, find_fmv_triggers, format_instruction} for downstream tooling. The InsnInfo::MenuCtrl { kind: MenuCtrlKind::FmvTrigger { fmv_id }, .. } variant carries the operand of the 0x4C 0xE2 op for callers who want to grep for cutscene triggers across the corpus.

CAVEAT - scene-event-scripts / scan-prot walk a NON-field-VM structure. The 0xFFFF 0x0000 lead is a per-record header sentinel, and the mode skips it before walking the record body - but those records are the word-aligned actor/event structure, not field-VM bytecode (see the "On-disc form" note above), so the disassembly is mostly decode error with coincidental matches. Any 0x4C 0xE2 FMV trigger these modes report inside a prescript record is a false positive. The genuine FMV triggers are pinned structurally (the exhaustive mode-write sweep + the disc-decoded fmv_dispatch table); the per-scene FMV-id remains capture-blocked.

FMV-trigger sites - exhaustive backward sweep

A grep across every Ghidra dump in the corpus for writes to the global game-mode word _DAT_8007B83C = 0x1A (the StrInit mode that boots the str_fmv overlay) finds only two distinct writers. Both are codified in legaia_engine_vm::cutscene_trigger as FMV_TRIGGER_SITES:

LabelFunctionMode-write addrFMV-id sourceTrigger condition
field_vm_op_4c_e2 FUN_801DE840 0x801E3104 decode_u16_be(pc+1) from field-VM bytecode Field-VM hits the byte sequence 0x4C 0xE2 lo hi; reached via JT chain 0x801CEE60 (high nibble 0xE) → 0x801CF008 (low nibble 0x2) → label 0x801E30E4.
title_attract_loop FUN_801DE234, case 0x10 0x801E0F50 Hardcoded 0 (= MV1.STR, intro) Title-screen idle countdown DAT_801ef16c underflows.

FUN_801E30E4 has zero static callers. It is a label inside FUN_801DE840, not a callable subroutine. Ghidra promotes it to a FUN_ symbol because the JT entry at 0x801CF008[2] resolves there; the actual control flow is the dispatch chain above. A direct grep -rn 'jal 0x801e30e4' ghidra/scripts/funcs/ returns zero matches.

The per-scene 0x4C 0xE2 trigger assignment is disc-sourced: the ops live LZS-compressed inside each scene's MAN (which is why a raw bytewise PROT scan missed them), and walking the decompressed partition-1 scripts recovers the literal fmv_id operands for all eight trigger scenes (town01 / garmel / deroa / chitei2 / dohaty / town0d / uru / jouine) - see the cutscene page. An earlier "reconstructed at scene-load time" reading is falsified.

See also