Files
Syplheed-Reborn/docs/re/structures/movie-subtitles.md
MechaCat02 95de29f290 feat(formats,viewer): movie subtitles, voice decode, and the movie manifest
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>
2026-07-19 20:15:41 +02:00

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.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<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.tbl0x6F2D9663, subtitle_hokyu_DS_s02A.tbl0x3662B1F8, subtitle_RT01C_1.tbl0x756F69FB.
  • 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_<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\, not Movie. A VOICE_<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..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 <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" 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)
<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_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 <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.