# 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/.pak` + `.p00` — per-language **subtitle timing tracks** (`eng`, `jpn`, `deu`, `fra`, `esp`, `ita`) plus the caption font. - `dat/GP_MAIN_GAME_.pak` + `.p00` — per-language **caption TEXT** (`E`=Eng, `J`=Jpn, `D`=Deu, `F`=Fra, `I`=Ita, `S`=Esp). - `dat/tables.pak` entry `ADVERTISE_MOVIE` (hash `0x5B983A08`) — the master **movie manifest**: all 101 `.wmv` names 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

.wmv` | Stage `NN` **story** cutscene, part `P` (A/B/C…) — e.g. `S02C` = stage 2, 3rd story scene | | `RT

.wmv` | Stage `NN` **radio / briefing** transmission, part `P` (`_1`/`_2` = split clips) | | `hokyu__s

.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 — `.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_.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**: bytes `5A 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_ MSG_DEMO_ …`. So the track says *which* demo-message shows *when*, not the text itself. ## Caption text — `GP_MAIN_GAME_.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___` (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 `.wmv` optionally followed by `+….prt` (overlay art), `+SUBTITLE_.tbl`, and a bare `VOICE_`. Grouping the pool on `.wmv` (records are emitted in order) recovers `movie → Option` without decoding the IDXD record binary (`crate::movie_manifest`). The voice token is **not** always `VOICE_`, 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_` in `\Movie\`. - **5 `hokyu_*` resupply movies bind to in-mission radio clips** — e.g. `hokyu_LS_s02A → VOICE_D_450`, which lives in `\etc\`, *not* `Movie`. A `VOICE_` 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 `\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..454` exist (no 44x/45x neighbours). Decoded durations are suspicious — `450`=2.8s, `451`=1.6s, `452`=2.2s, but `453`=**0.14s**, `454`=**0.43s** — far too short for the spoken line, so these `.slb` banks 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 `\…\.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"` before `01: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) ─ nh("subtitle_.tbl") ─→ .pak track track (IXUD) → [ (MSG_DEMO_, timecode), … ] MSG_DEMO_ → GP_MAIN_GAME_ → "the localized caption line(s)" ``` ## Coverage - 92 / 97 movies have a subtitle track. - **66 movies carry timed `MSG_DEMO` captions** — 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 `.pak`, resolves each movie's track, cross-references `GP_MAIN_GAME_` text, and prints per-movie timed transcripts + the movie→mission table. ## In-mission dialogue (future work) `GP_MAIN_GAME_.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.