dat/tables.pak holds a 5798-entry SOUNDS record (cue name -> sound id) and a 5135-entry FILES record (.slb bank paths). Cue names are the join key, so a script message id now resolves all the way to the bank that voices it: MSG_VOICE_D_257 -> VOICE_D_257 -> 6945 -> jpn\etc\VOICE_D_257.slb. The prefix rule is MSG_ -> VOICE_, not strip-MSG_. My first rule was the latter; it left 88 names unresolved and I was about to write those families up as text-only announcements, until VOICE_TCAF_592.slb turned up in FILES and refuted it. Corrected rule resolves 1326 of 1338, and SOUNDS and FILES agree on exactly the same 12 absentees. Separately, MSG_DEMO_* is driven by its own IDXD tables in the language packs, which carry speaker, portrait, on-screen seconds and audio cue per page. Field count is 9*PageCount+2 for all 7 distinct PageCounts; 1252/1252 caption-key slots match <ID>_<page>_<line>; the 78 multi-page records equal the 78 counted independently from the caption side; 138 ids close exactly against the caption table both ways. Does not settle the known VOICE_D_452 wrong-recording case -- every cue id is distinct, so bank sharing is not happening at this layer.
90 lines
4.0 KiB
Markdown
90 lines
4.0 KiB
Markdown
# The cutscene message table — speaker, portrait, timing and audio cue
|
||
|
||
**✅ Settled 2026-08-26.** The `MSG_DEMO_*` family is not called from any stage
|
||
script; it is driven by its own IDXD tables in the language packs. Those tables
|
||
carry, per line of cutscene dialogue: **who says it, which portrait is shown,
|
||
how long it stays on screen, and which audio cue plays**.
|
||
|
||
This answers the question left open by
|
||
[isl-message-dialogue-link.md](isl-message-dialogue-link.md) — what drives
|
||
cutscene dialogue, given that built-in 64 never mentions it.
|
||
|
||
## Where it lives
|
||
|
||
32 IDXD objects per language pack (`dat/GP_MAIN_GAME_<lang>.pak`). Each holds a
|
||
`Generic` record with a `Count`, plus some number of `Message_NNN` records —
|
||
**149 in total** across the English pack, covering **138 distinct ids**.
|
||
|
||
## Record layout
|
||
|
||
A `Message_NNN` record has two named fields, `ID` and `PageCount`, and then
|
||
**9 positional fields per page**:
|
||
|
||
| slot | content | example |
|
||
|---|---|---|
|
||
| 0 | speaker | `CharacterNATALIE` |
|
||
| 1 | portrait | `FaceNATALIE_01` |
|
||
| 2 | constant `None` at all 149 records | `None` |
|
||
| 3 | on-screen seconds, 1.00 – 5.00 (mean 2.48) | `1.9` |
|
||
| 4 | audio cue, or empty | `DEMO_017` |
|
||
| 5–8 | the four caption keys of that page | `MSG_DEMO_600_000_00` … `_03` |
|
||
|
||
The field count is exactly **9 · PageCount + 2**, and that identity holds for
|
||
every one of the 7 distinct PageCount values present (1→11, 2→20, 3→29, 4→38,
|
||
5→47, 6→56, 8→74). That is what pins the per-page grouping.
|
||
|
||
## Evidence
|
||
|
||
* **1 252 of 1 252** caption-key slots equal `<ID>_<page>_<line>` exactly, with
|
||
**zero** mismatches. The grouping is not a guess about which slot is which.
|
||
* **78** records span more than one page — which independently equals the 78
|
||
multi-page `MSG_DEMO` ids counted from the *caption* side, by a different
|
||
method. Two measurements, same number.
|
||
* In **68 of those 78** the speaker changes between pages, confirming from the
|
||
data that successive pages are successive utterances rather than one long
|
||
speech.
|
||
* The id sets close exactly: **138 ids in the caption table, 138 in the message
|
||
table, none on either side without the other.**
|
||
* 28 distinct speakers, 98 distinct portraits.
|
||
|
||
## The audio cue (slot 4)
|
||
|
||
296 of 313 pages carry a cue, and there are **296 distinct values** — one per
|
||
page, never reused. It joins to the `SOUNDS` table described in
|
||
[sound-cue-table.md](sound-cue-table.md):
|
||
|
||
DEMO_017 -> sound id 8017
|
||
|
||
The numbering rule is `DEMO_nnn` → `8000 + nnn`, which holds for **286 of 286**
|
||
plain-numeric cues. The 9 remaining cues carry a letter suffix
|
||
(`DEMO_067A`, `DEMO_190A`–`C`, `DEMO_216A`–`C`, `DEMO_278A`–`B`) and are
|
||
assigned 8400–8408 in order. There is no unexplained residue.
|
||
|
||
Within a message, **77 of the 78** multi-page records number their cues
|
||
consecutively; one does not.
|
||
|
||
**A guess I had and dropped:** slot 4 looked at first like a movie reference,
|
||
since the values resemble the `DEMO_nnn` naming of a clip. It is not — there
|
||
are 97 `.wmv` files on the disc and their names look nothing like this
|
||
(`ADV.wmv`, `RT01A.wmv`), and the values are unique *per page*, which no movie
|
||
reference would be.
|
||
|
||
## What this does not settle
|
||
|
||
* **17 pages have no cue** (slot 4 empty). Silent, or voiced by another route,
|
||
is unmeasured.
|
||
* **Slot 2 is `None` at all 149 records**, so what it would hold otherwise is
|
||
unknown — a constant with no observed variation carries no information.
|
||
* **5 ids have more than one record** (`MSG_DEMO_600`–`604`), accounting for
|
||
the 11 extra records over 138 ids. Whether these are context variants or
|
||
duplicates was not investigated.
|
||
* **Nothing here was run.** This is a static read of the tables; the playback
|
||
order, and whether slot 3 is really the on-screen duration rather than an
|
||
audio length or a delay, has not been checked against the running game.
|
||
|
||
## Artifact
|
||
|
||
[`../data/cutscene-message-table.txt`](../data/cutscene-message-table.txt) —
|
||
all 313 pages with speaker, portrait, seconds, cue and text.
|
||
Regenerate with `tools/re-capture/sound_cues.py demo`.
|