Last iteration I read the scattered missing runs as cut dialogue. Checking the
other languages refutes that for most of them.
Only two voice languages exist: tables.pak holds eng\sounds.tbl (4418 banks)
and jpn\sounds.tbl (5136), and NO fra/deu/esp/ita. The other four
localisations are subtitle-only, which is itself a fact the corpus did not
record.
The English set is a strict subset of the Japanese one:
eng jpn
listed 4418 5136
resolved 4175 4383
missing 268 60
lacked by
the other 0 718
Every id missing in Japanese is also missing in English, and NONE is missing
in Japanese but present in English. So the 268 split cleanly:
208 exist in Japanese, not in English -- recorded in the original and
never dubbed. A localisation gap, not cut content.
60 absent from both -- genuinely cut, including all 44 VOICE_E_ (listed
0 times in either language) plus 16 others.
Per family eng/jpn: A 683/771, B 383/423, C 366/474, D 387/546. The 718
Japanese-only banks include whole families English never had -- VOICE_ADAN_
(110), VOICE_ZZZZ_ (104), VOICE_TCAF_ (41), VOICE_RHIN_ (27).
So VOICE_E_ survives as the one genuinely-cut family, and my "short runs of
cut dialogue" reading was wrong: those lines are on the disc, in Japanese. The
previous text is kept above the correction.
This is why the "measured on English only" limit was worth writing down rather
than quietly generalising -- the second language changed the conclusion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
13 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. - ❌ WITHDRAWN — 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. ❌ The guess that follows was REFUTED 2026-08-25: these banks are not multi-subwave-and-missed. The RIFF-magic count equals the number of sub-waves recovered in all five banks, so nothing between or after them is lost. The audio is missing because a large region precedes the first RIFF and the decoder searches for that magic — 87 % of453and 85 % of454sit in front of it, high-entropy and not padding. See voice-bank-leading-region.
⇒ 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.
✅ DEMO_* in a message's voice slot is NOT a sound bank
The dialogue message tables put a token in what reads as a voice slot, and 296
distinct ones are spelled DEMO_* rather than VOICE_*. They do not name audio:
| token family | distinct | resolve in sound.pak |
|---|---|---|
VOICE_* |
4443 | 4175 — 2360 under eng\Voice\, 1815 under eng\etc\ |
DEMO_* |
296 | 0 |
Zero resolve under any of the three directories (Movie, etc, Voice), and
prefixing the convention — looking up VOICE_DEMO_nnn — resolves 0 of 296
too. So the slot is not "the voice bank" but something that can hold either a
bank reference or a demo reference.
🟡 The tokens run DEMO_000 … DEMO_310, 287 distinct in [0, 310] — a dense
sequential id space, which is the shape of the demo id this file already uses in
MSG_DEMO_<demo>_<page>_<line> caption keys. I did not confirm the overlap:
those keys live in the IXUD wide-string blocks and the scan above only walked
IDXD objects, so it found 0 of them. Confirming it needs an IXUD-aware sweep.
❔ 268 of the 4443 VOICE_* tokens do not resolve either. That is a separate
gap and is not explained by the DEMO_* finding.
✅ 268 voice references on the disc name a bank that does not exist
Resolving every VOICE_* token the message tables use the way the engine does —
token → eng\sounds.tbl for its directory → sound.pak — gives 4175 resolved,
268 unresolved out of 4443 distinct tokens.
The unresolved ones are not listed in sounds.tbl at all. Nothing is listed
and then missing from the pak: of the 4175 that are listed, every one is
present. So the gap is in the manifest, not the archive.
By family, comparing the ids the scripts use against the ids sounds.tbl lists:
| family | ids used | listed | missing |
|---|---|---|---|
VOICE_A_ |
696 [0..772] |
683 | 14 |
VOICE_B_ |
391 [0..422] |
383 | 8 |
VOICE_C_ |
429 [0..489] |
366 | 63 |
VOICE_D_ |
508 [0..598] |
387 | 126 |
VOICE_E_ |
44 [0..43] |
0 | 44 — the whole family |
Two different shapes:
A/B/C/Dlose short runs scattered through an otherwise dense range —11,12,13,56,57,181,182,183,389…392. That is what cut dialogue looks like: lines written, referenced, and never recorded.VOICE_E_is absent entirely — 44 ids used, none listed, noVOICE_E_*anywhere in the manifest. A whole speaker or category, cut.
For the port: a missing voice bank is a retail condition, not a decode bug.
A reimplementation must tolerate an unresolvable VOICE_* reference and carry on,
the same way it must tolerate SUBTITLE_S12B.tbl resolving in no language.
❌ CORRECTED — most of those 268 are not cut, they are undubbed
The paragraph above reads the scattered runs as "cut dialogue". Checking the other languages refutes that for most of them.
Only two voice languages exist. tables.pak holds eng\sounds.tbl (4418
banks) and jpn\sounds.tbl (5136) and no fra / deu / esp / ita — the
other four localisations are subtitle-only.
And the English set is a strict subset of the Japanese one:
| eng | jpn | |
|---|---|---|
| banks listed | 4418 | 5136 |
| tokens resolved | 4175 | 4383 |
| tokens missing | 268 | 60 |
| banks the other language lacks | 0 | 718 |
Every id missing in Japanese is also missing in English; none is missing in Japanese but present in English. So the 268 split cleanly:
- 208 exist in Japanese and not in English — recorded in the original, never dubbed. A localisation gap, not cut content.
- 60 are absent from both — genuinely cut. That includes all 44
VOICE_E_, which is listed 0 times in either language, plus 16 others.
Per family, English against Japanese: VOICE_A_ 683/771, VOICE_B_ 383/423,
VOICE_C_ 366/474, VOICE_D_ 387/546. The 718 Japanese-only banks also include
whole families English never had — VOICE_ADAN_ (110), VOICE_ZZZZ_ (104),
VOICE_TCAF_ (41), VOICE_RHIN_ (27).
So VOICE_E_ remains the one genuinely-cut family, and the "short runs of cut
dialogue" reading was wrong: those lines are on the disc, in Japanese.