The movie cutscene subtitle + voice pipeline, driven by the ADVERTISE_MOVIE manifest (the authoritative movie -> subtitle -> voice index). Also flushes several sessions of local WIP (async viewer loading, grouped-pool XBG7/hero-ship decode, drawlog tooling). See docs/HANDOFF-movie-voice-subtitles-2026-07-19.md. Subtitles (movie_subtitle.rs): - Full movie->track->text chain; join multi-line captions sharing one timing (fixes S13A dropped "Look at it father" line); overlap-safe active_cues(); Latin-1 accents preserved. Voice (slb.rs): XACT .slb -> XMA1 RIFF; take the FIRST sub-wave bounded by its declared data size (fixes S10-S16 alternate-take garble); list_voice_clips. Manifest (movie_manifest.rs): parse ADVERTISE_MOVIE (0x5B983A08) for the real movie->voice binding (not always VOICE_<movie>; e.g. hokyu -> VOICE_D_* in etc\). Resolve the token's sound.pak path via sounds.tbl. DIRECT bindings only — the demo-id shared-clip fallback for unbound hokyu movies was verified WRONG in-game and reverted (unbound hokyu stay unvoiced; correct join key is an OPEN problem). Viewer: manifest-driven voice (movie player toggle + solo button), standalone "Voice Lines" browser, stacked caption overlay. Tests: 46 formats-lib + 11 viewer-lib + movie_manifest/movie_subtitle/slb disc tests; full workspace green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.6 KiB
Movie subtitles & the movie ↔ mission ↔ text chain
Reverse-engineered 2026-07-19 (static, from the extracted disc). The full chain that links a cutscene movie to its on-screen subtitle text is now closed.
Files involved
dat/movie/*.wmv— the cutscene videos. Named by mission (see below).dat/movie/<lang>.pak+<lang>.p00— per-language subtitle timing tracks (eng,jpn,deu,fra,esp,ita) plus the caption font.dat/GP_MAIN_GAME_<L>.pak+.p00— per-language caption TEXT (E=Eng,J=Jpn,D=Deu,F=Fra,I=Ita,S=Esp).dat/tables.pakentryADVERTISE_MOVIE(hash0x5B983A08) — the master movie manifest: all 101.wmvnames in mission-progression order, each bound to its subtitle track and (optionally) its voice bank (see below).
Movie filename → mission
Purely from the filename:
| Pattern | Meaning |
|---|---|
S<NN><P>.wmv |
Stage NN story cutscene, part P (A/B/C…) — e.g. S02C = stage 2, 3rd story scene |
RT<NN><P>.wmv |
Stage NN radio / briefing transmission, part P (_1/_2 = split clips) |
hokyu_<LS|DS>_s<NN><P>.wmv |
Stage NN resupply scene (hokyu = 補給). LS/DS = the two resupply-ship variants |
ADV.wmv |
Intro / title movie |
97 movies total: 27 story, 50 radio, 19 resupply, 1 intro. (Some referenced stages — s24, s27, RT16 — exist as keys but the .wmv isn't in this extract.)
Subtitle timing track — <lang>.pak
IPFB archive (IPFB, BE-u32 count, 16-byte header; TOC of
[name_hash u32][offset u32][size u32] triples, sorted by hash, into .p00).
- Track key =
name_hash("subtitle_<movie_basename>.tbl")(the hash lowercases internally, so basename case is irrelevant). This is the movie→track link. Verified:subtitle_S00A.tbl→0x6F2D9663,subtitle_hokyu_DS_s02A.tbl→0x3662B1F8,subtitle_RT01C_1.tbl→0x756F69FB. - The archive also holds RATC pre-rendered title-card / number textures
(
pwterop_s01a1.t32,pwrt_rt01_str.t32,pwnum0-9.t32) + one TrueType font. - Each track data block is
Z1+zlib: bytes5A 31("Z1"), a small header, then a raw zlib stream (78 DA/78 9C).zlib.decompress(blob[blob.find(b"\x78\xda"):]). - Decompressed = an IXUD container. Payload (UTF-16LE) is the timing sheet:
SUBTITLE MSG_DEMO_<demo> <mm:ss.cc> MSG_DEMO_<demo> <mm:ss.cc> …. So the track says which demo-message shows when, not the text itself.
Caption text — GP_MAIN_GAME_<L>.pak
Same IPFB+.p00. Among its ~1119 entries, 32 blocks are Z1+zlib → IXUD
string containers holding the movie caption text. Layout: IXUD, u32 version,
hash@0x08, count@0x14, then (recordhash,offset,len) triples, then a UTF-16LE
string region where each line is stored as text immediately followed by its
key MSG_DEMO_<demo>_<page>_<line> (captions wrap across _00,_01, …).
537 English lines recovered. Entries 1 & 19 are the IDXD schema records
(ID, PageCount, Character=speaker e.g. TCAFSUPPLY, Face, line refs) —
no text, just structure.
Movie → voice track: the manifest binding (ADVERTISE_MOVIE)
The ADVERTISE_MOVIE manifest is also the authoritative movie→voice index.
Its string pool emits, per movie, a run led by <movie>.wmv optionally followed
by <pak>+….prt (overlay art), <pak>+SUBTITLE_<movie>.tbl, and a bare
VOICE_<token>. Grouping the pool on .wmv (records are emitted in order)
recovers movie → Option<voice_token> without decoding the IDXD record binary
(crate::movie_manifest).
The voice token is not always VOICE_<movie>, so the manifest is required —
guessing both misses real bindings and invents tracks for silent movies:
- 83 / 101 movies have a voice token. Story/radio movies use
VOICE_<movie>in<lang>\Movie\. - 5
hokyu_*resupply movies bind to in-mission radio clips — e.g.hokyu_LS_s02A → VOICE_D_450, which lives in<lang>\etc\, notMovie. AVOICE_<movie>guess would never find these. - 18 movies have no direct voice token = 4 boot logos + 1 HD test pattern +
13
hokyu_*movies (incl.hokyu_DS_s13A). Only the manifest's direct bindings are trusted for playback.
Shared resupply voice — UNRESOLVED for unbound movies
The manifest directly binds only 5 resupply movies, each to a shared
VOICE_D_45x clip in <lang>\etc\:
| bound movie | subtitle demo | clip |
|---|---|---|
hokyu_LS_s02A |
600 | VOICE_D_450 |
hokyu_LS_s09A |
601 | VOICE_D_451 |
hokyu_DS_s02A |
602 | VOICE_D_452 |
hokyu_LS_s02H |
603 | VOICE_D_453 |
hokyu_DS_s07H |
604 | VOICE_D_454 |
The resupply cutscenes clearly share voice recordings (only the video varies
per mission), so the 13 unbound hokyu_* movies must reuse one of these — but
the correct join key is not yet known:
- Keying by subtitle demo id (so
hokyu_DS_s13A, demo 602 →VOICE_D_452) was tried and is WRONG — it plays the wrong recording in-game. Do not use. - Only
VOICE_D_450..454exist (no 44x/45x neighbours). Decoded durations are suspicious —450=2.8s,451=1.6s,452=2.2s, but453=0.14s,454=0.43s — far too short for the spoken line, so these.slbbanks are likely multi-subwave / not cleanly sliced by the current extractor (same class as the deferred B/C banks).
⇒ The unbound-hokyu voice mapping is open (needs either the real join key
from mission data, or a proper multi-subwave .slb decode + audio verification).
hokyu_LS_s24A/s27A have no subtitle track at all (stages absent from this
extract).
The token's sound.pak subdirectory is not fixed (Movie / etc / Voice),
so resolve it via sounds.tbl (which lists the full <lang>\…\<token>.slb path)
rather than assuming a directory. movie_manifest::resolve_voice_entry does the
full chain. Verified: all 83 resolved entries exist in sound.pak.
Caption packing quirks (parser must handle)
- Multi-line captions are split into consecutive text tokens that share one
trailing timing, e.g. S13A stores
"Look at it father"+"& beautiful isn't it"before01:14.80-01:17.60. Accumulate every text token since the last timing and join with\n; pairing strictly 1:1 silently drops all but the last line. - Overlapping spans: some tracks show two captions at once (an open-ended
radio line still up when the next range line starts). The viewer stacks every
cue active at
t(MovieSubtitles::active_cues) instead of showing only the first.
The full join
tables.pak / ADVERTISE_MOVIE → list of movies (mission order)
<movie> ─ nh("subtitle_<movie>.tbl") ─→ <lang>.pak track
track (IXUD) → [ (MSG_DEMO_<d>, timecode), … ]
MSG_DEMO_<d> → GP_MAIN_GAME_<L> → "the localized caption line(s)"
Coverage
- 92 / 97 movies have a subtitle track.
- 66 movies carry timed
MSG_DEMOcaptions — the radio (RT*) and resupply (hokyu_*) movies. These fully decode to timed text. - The 27 story (
S*) movies have a track but 0 timed captions — their text is delivered as the pre-rendered title-card textures (pwterop_*, burned styling), not MSG_DEMO lines.
Worked examples (English)
RT01C_1.wmv (Stage 1 radio, part C):
00:00.50 [14] We did it! Okay, all pilots follow my lead!
00:06.80 [15] Rhino Leader to ACROPOLIS. We made it through and we're coming
home. Roger. It's good to see you're all safe.
00:19.30 [17] Yeah, but Brandon ... Damn. There's only seven of us. …
hokyu_DS_s02A.wmv (Stage 2 resupply):
00:00.00 [602] Resupply complete. You are cleared for take-off!
Reusable extractor
tools/extract_movie_subtitles.py — parses <lang>.pak, resolves each movie's
track, cross-references GP_MAIN_GAME_<L> text, and prints per-movie timed
transcripts + the movie→mission table.
In-mission dialogue (future work)
GP_MAIN_GAME_<L>.pak is the global message store, not just movie captions:
its MSG_DEMO_* table also holds the in-mission radio/dialogue lines (same demo
id space). So the text of gameplay dialogue is already decodable with
[crate::movie_subtitle::build_demo_text]. What's missing is the trigger —
which demo id fires at which mission event — and that lives in the mission
data (mission scripts / GP_MAIN_GAME IDXD tables), not in the text pack. When
reversing mission data, look for demo-id references there to bind dialogue to
events; the IDXD "Message" schema records also carry Character (speaker) and
Face (portrait) per line.