Confidence

Confirmed. The dispatch-table stride and field layout are pinned from the disc bytes of PROT 0970 at its static base (selector sll v0,v0,0x5 at 0x801CEC9C) and byte-identical in the FMV-overlay-resident RAM capture; the play loop's field reads cross-validate every field. The retail trigger range (0..=8) is pinned by the per-STR FMV trigger corpus (nine save states, _DAT_8007BA78 ∈ 0..=8) - exactly matching the table's nine retail slots.

Directory-record cache (0x801CAE08, 24 B CdlFILE records)

What earlier captures read as a "compact MV-file table at 0x801CAE40" is libcd's CdSearchFile per-directory file cache (libcd is the CD-ROM library of PsyQ, the PlayStation SDK) - one CdlFILE record per file of the last-searched directory:

+0x00  u32       CdlLOC   - byte 0 = BCD minute, 1 = BCD second,
                            2 = BCD frame, 3 = zero
+0x04  u32       size     - file size in bytes (LE)
+0x08  char[16]  name     - "MV1.STR;1\0..." (null-padded)

starting with the . / .. entries at 0x801CAE08 / 0x801CAE20; the first named record sits at 0x801CAE38. The earlier name-first parse (name at +0x00, MSF at +0x10) was phase-shifted 8 bytes, pairing each name with the next record's location - manufacturing the apparent one-entry shift ("MV1 points at disc MV2", "MV6 points at XA15.XA"). At the CdlFILE phase every record is self-consistent. A title-screen capture shows the same cache holding the XA directory (XA1.XA;1..XA34.XA;1); the FMV capture shows the MOV directory. Convert CdlLOC to LBA with LBA = ((M×60)+S)×75 + F − 150.

The legaia_asset::str_fmv_table parser still reads the historical name-first window (its bcd_msf is the following record's location); treat it as a capture-forensics helper, not a format decoder.

Path string table (0x801CE810)

The dispatch slots' path-pointer field (+0x00) points into a packed null-terminated string table at the very start of the STR/MDEC overlay: the path_ptr values resolve against the overlay link base 0x801CE818 (legaia_asset::fmv_dispatch::STR_OVERLAY_BASE_VA). Nine paths in storage order:

OffsetString
+0x008\DATA\MOV.STR;1
+0x018\DATA\MOV15.STR;1
+0x02C\MOV\MV1A.STR;1
+0x03C\MOV\MV6.STR;1
+0x04C\MOV\MV5.STR;1
+0x05C\MOV\MV4.STR;1
+0x06C\MOV\MV3.STR;1
+0x07C\MOV\MV2.STR;1
+0x08C\MOV\MV1.STR;1

Three of the nine paths (\DATA\MOV.STR;1, \DATA\MOV15.STR;1, \MOV\MV1A.STR;1) are dev-only - the corresponding files are not on the retail disc.

Return-scene labels (0x801CE8AC)

The overlay data section carries seven CDNAME-shape labels: town0b, map01, chitei2, map02, jou, uru2, town0e. These are the post-FMV return scenes: after the play loop exits, the master dispatch (FUN_801CEA3C) copies the label for the just-played fmv_id into the next-scene name global 0x80084548, writes a spawn/door word to 0x80084540, and hands the game mode back to the field chain (see the mapping below).

FMV dispatch table (0x801D0A6C, 23 × 32 B)

The selector lives in the master dispatch FUN_801CEA3C:

0x801CEC94: lh   v0, -0x4588(s0)        ; v0 = (s16) _DAT_8007BA78
0x801CEC9C: sll  v0, v0, 0x5            ; v0 = fmv_id * 32
0x801CECA0: jal  FUN_801CF098
0x801CECA4:  _addu a1, v0, 0x801D0A6C   ; param_2 = &dispatch_table[fmv_id]

Each 32-byte record is [u32 path_ptr, u32 color_flag, u32 start_frame, u32 end_frame, u32 fb_x, u32 fb_y, u32 width, u32 height] (color_flag non-0 = 24-bit color; fb_x/fb_y = the VRAM decode-target rect, double-buffered at fb_y and fb_y + height). The play loop opens the file at path_ptr, seeks (start_frame − 1) × 10 sectors in (the 15 fps cadence), and streams until the demuxed frame number reaches end_frame - so one MVn.STR file carries several cutscenes by frame range.

This is static initialised data in the cutscene overlay (PROT entry 0970 - a numbered entry of PROT.DAT, the disc's single big archive), so it decodes straight from the disc - legaia_asset::fmv_dispatch reads it, pinned by the disc-gated fmv_dispatch_real test, and the windowed player uses the frame range to seek to the right segment (cutscene_av::fmv_segment_window).

An earlier reading used a 64-byte stride (a sll v0,v0,6 transcription error), pairing wrong slot halves - it concluded MV2/MV5 were never referenced and slots 5..11 pointed at cut files. That reading is superseded; the disc-parsed FmvTable is the authoritative resolver, mirrored by legaia_engine_core::cutscene::fmv_index_to_str_filename for the retail fmv_id 0..=8 slots.

_DAT_8007BA78 is a s16 written by the field-VM FMV-trigger op (0x4C 0xE2 lo hi …); see the cutscene subsystem for the full opcode trace.

Authoritative mapping

fmv_idMovieFramesPost-play hand-off
0\MOV\MV1.STR;11..0x53amode 0x16 (22 = CARD init) with _DAT_8007BB00 = 2; the intro/attract movie
1\MOV\MV2.STR;11..0xf4scene town0b, spawn word 0x0C
2\MOV\MV3.STR;11..0xe1scene map01, spawn word 0x55
3\MOV\MV3.STR;10xe2..0x1a4scene chitei2, spawn word 0x2C1
4\MOV\MV3.STR;10x1a5..0x27bscene map02, spawn word 0xF4
5\MOV\MV3.STR;10x27c..0x36amode 2 with _DAT_8007B8B8 = 2 (no scene-name write)
6\MOV\MV4.STR;11..0x152scene jou, spawn word 0x276
7\MOV\MV5.STR;11..0x288scene uru2, spawn word 0x1BC
8\MOV\MV6.STR;11..0x297scene town0e, spawn word 0x2E5
9\MOV\MV1A.STR;11..0xad4dev slot (file not on disc); mode 0x16 with _DAT_8007BB00 = 1
10\DATA\MOV15.STR;11..0xad4dev slot; mode 0
11..=22\DATA\MOV.STR;11..0x64dev multi-window test slots (varying fb_x/fb_y/width)

MV3.STR's four segments abut exactly (0xe1+1 = 0xe2, 0x1a4+1 = 0x1a5, 0x27b+1 = 0x27c). Every movie on the retail disc is dispatched.

Per-STR FMV trigger corpus

The corpus carries nine save states, each captured immediately before an FMV begins playing, one per _DAT_8007BA78 value (fmv_id ∈ 0..=8 - exactly the retail slot range). Each save pins:

  • _DAT_8007BA78 = expected_fmv_id (s16 LE)
  • _DAT_8007B83C = 0x1A (StrInit game mode)
  • _DAT_8007BAC8 = 2000 (BGM ID; global pool index 0)
  • Active scene = map01 (one of the seven return-scene labels)
  • recover_base() = 0x80139530 (map01's field-pack base)

The 0x4C 0xE2 lo hi byte sequence does not appear in the field-pack RAM region for any save in the corpus - the saves were generated by debug-menu-driven trigger paths, not by stepping the field VM through a per-scene FMV trigger op. The corpus pins the trigger-side state across the full 0..=8 range. The two direct _DAT_8007BA78 store sites (the 4C E2 handler and the title-attract tick) are corpus-exhaustive (raw-byte scan of all 1235 PROT entries, every addressing form) - the debug path writes the global through the dev-menu's register-pointer editor (FUN_801DBD04 family, field overlay 0897), invisible to static addressing-form scans.

The corpus is codified at legaia_engine_core::capture_observations::cutscene_trigger_corpus and exercised by the disc-gated test cutscene_trigger_corpus_pins_fmv_id_across_nine_saves.

Per-scene trigger assignment (disc-sourced)

Which fmv_id each scene fires is inline script data, not a runtime value: the trigger op carries its id as a literal i16 operand, so walking every scene MAN's partition-1 scripts with the field-VM disassembler recovers the full assignment straight from the disc (legaia_engine_core::man_field_scripts::scene_fmv_triggers, the 0x3F-destination walk's sibling; pinned by the disc-gated scene_fmv_triggers_disc test over all 88 scene MANs):

Scene (extraction entry)fmv_idMovie segmentReturns to
0004_town011MV2.STRtown0b
0095_garmel2MV3.STR 1..0xe1map01
0606_deroa3MV3.STR 0xe2..0x1a4chitei2
0706_chitei23MV3.STR 0xe2..0x1a4chitei2
0218_dohaty4MV3.STR 0x1a5..0x27bmap02
0348_town0d6MV4.STRjou
0435_uru7MV5.STRuru2
0689_jouine8MV6.STRtown0e

One trigger op per scene; no other scene MAN carries one. fmv_id 0 (the MV1.STR intro) fires from the title/new-game path rather than a scene script. fmv_id 5 (the fourth MV3.STR segment, the "stay in mode 2" slot) appears in no scene MAN partition-1 script; the raw-byte 4C E2 candidate inside taiku's uncompressed scene structures is the matching suspect. The earlier "vestigial triggers at cut movies" reading of town0d / uru / jouine is superseded - they play MV4 / MV5 / MV6.

Rust API

use legaia_asset::fmv_dispatch::FmvTable;

// Decode the dispatch table straight from the PROT 0970 entry bytes.
let overlay = std::fs::read("extracted/PROT/0970_xxx_dat.BIN")?;
let table = FmvTable::from_str_overlay(&overlay).expect("dispatch table");

// fmv_id (the value the field VM writes to _DAT_8007BA78) -> movie + range.
let e = table.entry(1).unwrap();
assert_eq!(e.engine_path(), "MOV/MV2.STR");
assert_eq!((e.start_frame, e.end_frame), (1, 0xf4));

// Dev slots decode too but engine_path() declines them.
assert_eq!(table.engine_path(10), None); // MOV15.STR, not on the retail disc

See also