This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/structures/movie-subtitles.md
Sylpheed RE agent fedb31a5f9 re: rebuild the movie manifest on the record table — the old counts measured pool strings
movie_manifest::parse now reads BASE_INFO's positional field keys (the game's
own cutscene ids, stage*100 + slot) and follows each to its record, instead of
scraping the string pool. The pool stores each distinct string once, so a
REPEAT reference produced no token and read as "no binding".

That single cause explains every wrong cell: 13 later references to
VOICE_D_450..454, two to SUBTITLE_hokyu_LS_s11A.tbl, and MS01A's share of
pwterop_s01a.prt. All 18 hokyu movies are bound, not five.

Counts, verified independently by me against the disc before recording:
104 cutscene SLOTS binding 101 distinct MOVIES; 99 slots / 96 movies with a
voice track, 99 / 96 with a subtitle, 22 / 22 with a telop. The docs' old
94 / 83 / 21 are exactly the counts of DISTINCT POOL STRINGS -- not wrong
measurements, measurements of the wrong thing. Three denominators were being
conflated; the new test pins all three.

Two assertions in movie_manifest_disc.rs were false and are corrected:
hokyu_DS_s13A binds VOICE_D_452 and resolves to eng\etc\VOICE_D_452.slb. The
in-game verdict that rejected that value tested an INFERENCE from a shared
demo id, on a decoder that discards 85-87% of banks in this class -- see
voice-bank-leading-region.md, committed earlier today.

The ~104 script ids are no longer open: they are literal positional keys,
each naming its record, and all 104 resolve. The old "counts differ by three,
positional pairing does not work" has a concrete cause -- three resupply
movies are bound by TWO slots each.

Also corrected: the naming convention has 3 subtitle exceptions (s24A/s27A
borrow s11A's track) and 18 voice exceptions, not one and five.

The legacy scraper is kept as a fallback for blobs with no record table, so
the synthetic unit fixtures still exercise it.

Artifacts: examples/movie_map_csv.rs regenerates the CSV, now slot-keyed
(104 rows; the movie-keyed version silently dropped one slot of each
duplicate). Disc tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-25 23:13:41 +00:00

8.9 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.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_<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.
  • ❌ 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..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. ❌ 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 % of 453 and 85 % of 454 sit 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" 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.