re: the sound-cue table, and cutscene dialogue's speaker/portrait/timing

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.
This commit is contained in:
Sylpheed RE agent
2026-08-26 03:24:15 +00:00
parent 8379bc57b8
commit b5fa88c0a2
4 changed files with 706 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
# 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` |
| 58 | 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 84008408 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`.

View File

@@ -0,0 +1,94 @@
# The sound-cue table, and how dialogue binds to a recording
**✅ Settled 2026-08-26.** `dat/tables.pak` carries the game's cue index. It
closes the join from a message id to the `.slb` bank that voices it — the
missing link between the script/caption side and the audio side.
## The table
One IDXD object in `dat/tables.pak` (373 680 bytes, 5 records, 10 937 fields)
holds:
| record | shape | content |
|---|---|---|
| `SETTINGS` | 3 fields | `PATH` = `game:\dat\sound.pak+`, `PARAM` = `Pj_Silph.xgs`, and a positional `BANK_SE` |
| `BANK_SE` | 1 positional | `Static.slb` |
| `FILES` | 5 135 positional | bank paths — `jpn\Voice\VOICE_TCAF_592.slb`, `BGM_001.slb`, … |
| `STAGES` | **0 fields** | present but empty |
| `SOUNDS` | 5 798 **named** | cue name → numeric sound id |
`SOUNDS` is the only record on the disc found so far that is entirely named
fields, and the names are the join key.
Cue families and their id ranges — the id space is partitioned by family, with
no overlap:
VOICE n=5044 1500 .. 7331
SE n= 322 1 .. 901
DEMO n= 295 8000 .. 8408
BR n= 101 8500 .. 8600
BGM n= 32 1001 .. 1109
JNGL n= 3 1201 .. 1203
5 797 of the 5 798 values are numeric and **all 5 797 are distinct** — the ids
are unique, so the id is not how banks get shared. One cue's value is
non-numeric.
The id is **not** an index into `FILES`: ids run to 8 600 and `FILES` has only
5 135 entries. `FILES` is reached by *name*, not by id.
## The join: a message id to a recording
MSG_VOICE_D_257 (built-in 64's operand, see isl-message-dialogue-link.md)
-> cue VOICE_D_257 `MSG_` becomes `VOICE_`
-> id 6945 SOUNDS
-> bank jpn\etc\VOICE_D_257.slb FILES, matched on stem
The prefix rule has one wrinkle, and getting it wrong is easy:
* **`MSG_` is replaced by `VOICE_`** — it is not simply stripped.
* A name that already carries `VOICE_` keeps the one it has rather than
doubling it: `MSG_VOICE_D_257``VOICE_D_257`, but `MSG_TCAF_592`
`VOICE_TCAF_592` (not `TCAF_592`).
**I got this wrong first.** Stripping `MSG_` resolved 1 250 of 1 338 script
message names and left 88 unresolved, and the 88 were exactly the four-letter
ship families (`ADAN` 32, `ADPL` 27, `TCAF` 12, `ACRO` 3, `RHIN` 2) — which I
was ready to write up as "text-only announcements with no recording". Seeing
`jpn\Voice\VOICE_TCAF_592.slb` in `FILES` refuted that: the recordings exist,
my rule was wrong. Under the corrected rule the count is **1 326 of 1 338**.
`SOUNDS` and `FILES` agree exactly — the same 12 names are absent from both,
and **every one of the 5 135 bank files has a cue** (zero orphans). 663 cues
have no bank file; those are `SE`/`BGM`-family cues living in other banks.
## The 12 script lines with no recording
MSG_VOICE_A_273 MSG_VOICE_D_183
MSG_VOICE_D_600 … MSG_VOICE_D_608 MSG_VOICE_D_611
All twelve are in the `VOICE` family, have caption text, are called from the
stage scripts, and have neither a cue nor a bank. Ten of them form the near
contiguous block `D_600``D_611`. Whether they are subtitle-only by design or
a late cut is **not settled** — the shape (a contiguous high block) suggests
lines added after the recording sessions, but that is a guess, not a
measurement.
## What this does *not* settle
* **The known wrong-recording case is not explained by this table.**
`VOICE_D_452` — the generic "Resupply complete" line documented in
[voice-bank-leading-region.md](../voice-bank-leading-region.md) — has its own
unique id (7140) and its own bank. Since every id is distinct, bank sharing
is **not** happening at the cue level, so that symptom still has to be
explained inside the bank/`sound.pak` layer.
* **`STAGES` is empty**, and what would have populated it is unknown.
* **`SOUNDS`' one non-numeric value** was not chased down.
* Only the English pack and the `jpn\` bank paths were examined. Every path in
`FILES` begins with `jpn\`, which is consistent with the already-recorded
finding that the voice set is Japanese, but the other language packs'
tables were not diffed against this one.
## Tool
tools/re-capture/sound_cues.py summary | resolve <MSG_id> | demo | unbound