In-RAM STR FMV file tables Confirmed
When the game plays a movie - the intro, the Mist rolling in - which file does it open, and
where does the player go afterwards? The answer is a 23-slot FMV dispatch table inside the cutscene
overlay (a chunk of code and data the game loads into RAM only when a movie plays). Each numbered cutscene
(fmv_id) maps to a movie file path, a frame range - one .STR file
(the PlayStation's streaming-movie format) can carry several cutscenes back-to-back, split by frame numbers -
and a return scene. Nine slots cover every movie on the retail disc; the rest are leftover developer test
slots pointing at files that never shipped. A second, unrelated table shares the overlay's data section and
misled earlier captures: libcd's generic directory-record cache at 0x801CAE08 (PsyQ
CdlFILE records - not an FMV structure at all). The dispatch table at 0x801D0A6C (32
bytes per slot) is static overlay data, so it decodes straight from the disc; parser
legaia_asset::fmv_dispatch.
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:
| Offset | String |
|---|---|
+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_id | Movie | Frames | Post-play hand-off |
|---|---|---|---|
| 0 | \MOV\MV1.STR;1 | 1..0x53a | mode 0x16 (22 = CARD init) with _DAT_8007BB00 = 2; the intro/attract movie |
| 1 | \MOV\MV2.STR;1 | 1..0xf4 | scene town0b, spawn word 0x0C |
| 2 | \MOV\MV3.STR;1 | 1..0xe1 | scene map01, spawn word 0x55 |
| 3 | \MOV\MV3.STR;1 | 0xe2..0x1a4 | scene chitei2, spawn word 0x2C1 |
| 4 | \MOV\MV3.STR;1 | 0x1a5..0x27b | scene map02, spawn word 0xF4 |
| 5 | \MOV\MV3.STR;1 | 0x27c..0x36a | mode 2 with _DAT_8007B8B8 = 2 (no scene-name write) |
| 6 | \MOV\MV4.STR;1 | 1..0x152 | scene jou, spawn word 0x276 |
| 7 | \MOV\MV5.STR;1 | 1..0x288 | scene uru2, spawn word 0x1BC |
| 8 | \MOV\MV6.STR;1 | 1..0x297 | scene town0e, spawn word 0x2E5 |
| 9 | \MOV\MV1A.STR;1 | 1..0xad4 | dev slot (file not on disc); mode 0x16 with _DAT_8007BB00 = 1 |
| 10 | \DATA\MOV15.STR;1 | 1..0xad4 | dev slot; mode 0 |
| 11..=22 | \DATA\MOV.STR;1 | 1..0x64 | dev 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_id | Movie segment | Returns to |
|---|---|---|---|
0004_town01 | 1 | MV2.STR | town0b |
0095_garmel | 2 | MV3.STR 1..0xe1 | map01 |
0606_deroa | 3 | MV3.STR 0xe2..0x1a4 | chitei2 |
0706_chitei2 | 3 | MV3.STR 0xe2..0x1a4 | chitei2 |
0218_dohaty | 4 | MV3.STR 0x1a5..0x27b | map02 |
0348_town0d | 6 | MV4.STR | jou |
0435_uru | 7 | MV5.STR | uru2 |
0689_jouine | 8 | MV6.STR | town0e |
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