343 English headerless banks carry a valid seek chunk, which would lift dialogue duration coverage from 62% to ~75%, with a median 3.25 s that looks exactly like in-mission voice. It is wrong. A seek sits immediately after its OWN data, so the implied wave start is seek_pos - packets*2048 -- and for 277 of the 343 that start lies BEFORE the entry begins, meaning the seek describes the previous bank's wave. Applying it would attach a neighbour's duration to 81% of the recovered rows. The 66 that start inside the entry are not proven to be that entry's wave either; 'starts inside' is necessary, not sufficient. Recorded as refused rather than applied with a caveat, because a plausible median is exactly what has misled this work twice today. Recovering these needs the bank-to-wave assignment settled first.
214 lines
9.9 KiB
Markdown
214 lines
9.9 KiB
Markdown
# ISL `request_script_message` → caption text
|
||
|
||
**✅ Settled 2026-08-26.** Every mission-script message call resolves to the
|
||
dialogue line it plays. The 28 stage scripts are readable as dialogue.
|
||
|
||
## The link
|
||
|
||
Built-in **64** (`request_script_message`) takes one operand in slot 0: a
|
||
two-word symbol reference into **symbol table 1**. The symbol's name is a
|
||
message id such as `MSG_VOICE_D_257`. The text lives in the language pack's
|
||
IXUD blocks (`dat/GP_MAIN_GAME_<lang>.pak`) as a **named field** whose key is
|
||
`<id>_<page>_<line>`, read via the record/field table documented in
|
||
[idxd-container.md](idxd-container.md).
|
||
|
||
MSG_VOICE_D_257 → field key `MSG_VOICE_D_257_000_00` → "Katana! Damn it!"
|
||
|
||
## Evidence
|
||
|
||
Measured over all 28 `StageNN.ssb`:
|
||
|
||
| | |
|
||
|---|---|
|
||
| built-in 64 call sites | **2683** |
|
||
| slot 0 not staged | 0 |
|
||
| operand not in symbol table 1 | 0 |
|
||
| operand symbol type | **6** at all 2683 sites |
|
||
| distinct message names | 1338 |
|
||
| names with caption text at `_000_00` | **2683 / 2683 (100 %)** |
|
||
|
||
There is no residue: no call site takes a different operand shape, no name is
|
||
of a type other than 6, and no name is missing from the caption table. That
|
||
totality is the result — a partial rate would have meant the operand encoding
|
||
was only sometimes what I thought it was.
|
||
|
||
Name families and their share of call sites:
|
||
|
||
MSG_VOICE 2407 MSG_ADAN 118 MSG_ACRO 82
|
||
MSG_ADPL 38 MSG_TCAF 35 MSG_RHIN 3
|
||
|
||
`MSG_VOICE_*` is crew/wingman dialogue; the four-letter families are
|
||
ship-side announcements (`ACRO` = ACROPOLIS, `ADAN`/`ADPL` = ADAN,
|
||
`TCAF`, `RHIN` = Rhino flight).
|
||
|
||
## Page and line structure — and a correction to this page's first tool
|
||
|
||
**✅ Measured 2026-08-26**, on the English pack. A caption id is keyed
|
||
`<id>_<page>_<line>`:
|
||
|
||
* a **page** is one subtitle box with **four line slots** (4 506 pages allocate
|
||
4, 263 allocate 3 — no other width occurs). ⚠️ That counts *slots*, not text;
|
||
an English page carries **1 to 4** actual lines — see the correction below;
|
||
* successive **pages are successive utterances**, and may be different
|
||
speakers. A multi-page id is therefore a whole exchange, not one long line:
|
||
|
||
MSG_DEMO_009 page 0 No way! There's more of them?
|
||
page 1 Rhino Leader to ACROPOLIS! There's a whole fleet of them now!
|
||
page 2 What? Is there any way you can get out of there?
|
||
page 3 If we get rid of those fighters, we can cut our way out of here.
|
||
|
||
Of 4 091 distinct ids, **452 span more than one page** (374 `MSG_VOICE`,
|
||
78 `MSG_DEMO`), up to 8 pages.
|
||
|
||
**This refuted the first version of `isl_dialogue.py`**, committed earlier the
|
||
same day, which read only page `000`. **356 of the 1338 script message names
|
||
are multi-page**, so a quarter of the dialogue it printed was silently
|
||
truncated to its opening utterance. The tool now walks pages until one is
|
||
empty. The 2683/2683 resolution figure above is unaffected — it counted ids
|
||
that *have* text, and every id does have a page `000` — but the *rendered*
|
||
text was incomplete, and the caveat this page originally carried ("whether any
|
||
id uses a page other than `000` is unmeasured") understated it as a question
|
||
when it was already a defect.
|
||
|
||
## Why this was not obvious earlier
|
||
|
||
The caption table only became readable on 2026-08-25, when `build_caption_text`
|
||
was switched from scraping adjacent string-pool tokens to reading the IXUD
|
||
**field** table — which took the extractable line count from 537 to 8800
|
||
(8800 of 8800 distinct keys). Before that the great majority of these 1338
|
||
names had no text to resolve *to*, and the 100 % figure was unreachable.
|
||
|
||
## What this does not settle
|
||
|
||
* **Which recording plays.** The caption is the *text*; the voice bank binding
|
||
is a separate and still-unresolved question — see
|
||
[voice-bank-leading-region.md](voice-bank-leading-region.md) and the
|
||
known case of a generic line playing against a specific subtitle.
|
||
* **Which page a `MSG_DEMO_*` id belongs to.** The `DEMO` family is not called
|
||
from the stage scripts at all — 78 of its ids are multi-page, so something
|
||
else drives cutscene dialogue. That driver is unidentified.
|
||
* ~~**The other five languages.** Only the English pack was swept.~~ ✅ Swept
|
||
2026-08-26 — see below.
|
||
* **Ordering.** The tool prints call sites in file order, which is *script*
|
||
order, not necessarily *play* order — phases and branches are not resolved.
|
||
|
||
## Tool
|
||
|
||
tools/re-capture/isl_dialogue.py <StageNN.ssb> [GP_MAIN_GAME_E.pak]
|
||
|
||
Sample output committed at [../data/isl-stage02-dialogue.txt](../data/isl-stage02-dialogue.txt)
|
||
(Stage 02, 213 message calls, 213 with text).
|
||
|
||
|
||
## ✅ All six language packs swept (2026-08-26)
|
||
|
||
| pack | distinct `MSG_*` keys | with non-empty text | ids | pages |
|
||
|---|---|---|---|---|
|
||
| **E** English | 18 813 | **8 800** | 4 091 | 4 758 |
|
||
| **D** German | 18 813 | — | 4 091 | — |
|
||
| **F** French | 18 813 | — | 4 091 | — |
|
||
| **I** Italian | 18 813 | — | 4 091 | — |
|
||
| **S** Spanish | 18 813 | — | 4 091 | — |
|
||
| **J** Japanese | 14 295 | 7 266 | 4 087 | 4 765 |
|
||
|
||
The five European packs have **byte-for-byte identical key sets** — zero keys
|
||
missing, zero extra, in any direction. Only Japanese differs, and the difference
|
||
is not missing content:
|
||
|
||
* its **id** set matches to within 5 missing and 1 extra of 4 091;
|
||
* its **page** count is 4 765 against English's 4 758 — the same;
|
||
* its **lines per page** are fewer: mean **1.52** vs **1.85**, and it never uses
|
||
a 4-line page where English has 113.
|
||
|
||
So Japanese carries the same dialogue and simply **wraps into fewer lines**,
|
||
which is what a language without inter-word spaces does in a fixed-width box.
|
||
The 4 521 "missing" keys are line slots that were never needed. ❌ A reading of
|
||
"Japanese is missing 4 521 captions" would be wrong.
|
||
|
||
### ⚠️ Correcting my own wording elsewhere
|
||
|
||
**This page**, further up, said a page holds "3 or 4 wrapped lines (4 506 of 4,
|
||
263 of 3 — no other width occurs)" — I first misattributed that sentence to
|
||
[cutscene-message-table.md](cutscene-message-table.md) while correcting it,
|
||
which is its own small carelessness. It measured **key slots**, which the
|
||
records allocate four at a time. Counting lines that actually carry text, an
|
||
English page holds **1 to 4**:
|
||
|
||
1 line: 1 574 2 lines: 2 439 3 lines: 632 4 lines: 113
|
||
|
||
Both numbers are true of different things, but "holds 3 or 4 wrapped lines"
|
||
reads as a statement about the text and is wrong as such. A page has up to four
|
||
slots and usually fills one or two.
|
||
|
||
This also reconciles the **8 800** figure quoted throughout the corpus: it is
|
||
distinct English keys *with non-empty text*, out of 18 813 keys in total. The
|
||
larger number is not a contradiction — most slots are empty.
|
||
|
||
## ✅ Dialogue with audio durations, all 28 stages
|
||
|
||
**2026-08-26.** [`../data/stage-dialogue-with-durations.txt`](../data/stage-dialogue-with-durations.txt)
|
||
joins three separately decoded systems — the ISL script's built-in 64 call
|
||
sites, the caption table, and the sound-bank `seek` tables:
|
||
|
||
offset | message id | eng s | jpn s | caption
|
||
|
||
Duration is the bank's last cumulative sample over its sample rate. **Nothing is
|
||
decoded**; every number is read.
|
||
|
||
Coverage over the 2 683 call sites: **1 663 (62.0 %) have an English duration**
|
||
and **2 198 (81.9 %) a Japanese one**. The gap is the headerless banks, which
|
||
carry no `seek` table — and which language lacks one differs per line, so the
|
||
two columns are complementary rather than nested.
|
||
|
||
**Two columns, deliberately.** `FILES` names the `jpn\` banks and the game
|
||
substitutes the language directory, so a line's English and Japanese recordings
|
||
are different files of different lengths. The caption text is English, so `eng`
|
||
is the column that matches it. Reporting a single duration here would have
|
||
silently attached Japanese timings to English text — the first version of this
|
||
artifact did exactly that, which is why it was regenerated.
|
||
|
||
The difference is not academic:
|
||
|
||
MSG_VOICE_A_065 "They got Leader!" eng 1.78 s jpn 2.46 s
|
||
MSG_VOICE_D_257 "Katana! Damn it!" eng - jpn 2.59 s
|
||
MSG_VOICE_D_342 "Katana, Ellen, good work." eng 4.18 s jpn -
|
||
|
||
❔ Not settled: the 38 % of call sites with no English duration. They are
|
||
headerless banks. **A `seek`-based recovery was tried and does not work** — see
|
||
below.
|
||
|
||
### ❌ Recovering headerless durations from the `seek` chunk — tried, refused
|
||
|
||
A headerless bank has no `RIFF`, so no `data` size to read. But a `seek` chunk
|
||
describes the wave *ending* at its own position, so an entry carrying one looks
|
||
like it should yield a length for free. Across the bank set:
|
||
|
||
| | eng | jpn |
|
||
|---|---|---|
|
||
| has a `RIFF` (already covered) | 3 686 | 4 301 |
|
||
| headerless **with** a valid `seek` | **343** | 419 |
|
||
| headerless, no `seek` | 353 | 380 |
|
||
| no entry for that language | 753 | 35 |
|
||
|
||
343 recoverable English durations would lift coverage from 62 % to about 75 %,
|
||
and the numbers look right — median **3.25 s**, p90 5.18 s, which is exactly the
|
||
shape of in-mission voice.
|
||
|
||
**It is still wrong.** A `seek` sits immediately after *its own* data, so the
|
||
implied wave start is `seek_pos − packets × 2048`. Checking that:
|
||
|
||
wave starts BEFORE the entry begins : 277 of 343
|
||
wave starts inside the entry : 66 of 343
|
||
|
||
**For 277 of the 343, the `seek` describes the previous bank's wave, not this
|
||
entry's.** That is the same pairing rule that made my earlier `seek` readings
|
||
fail; taking the length anyway would attach a neighbour's duration to 81 % of the
|
||
recovered rows. Even the 66 that start inside the entry are not *proven* to be
|
||
that entry's own wave — "starts inside" is necessary, not sufficient.
|
||
|
||
The median 3.25 s is precisely the kind of plausible-looking figure that has
|
||
already misled this work twice today, so the approach is recorded as **refused**
|
||
rather than applied with a caveat. Recovering these lengths needs the bank→wave
|
||
assignment settled first — the open question `auto/slb-loader` records as *"which
|
||
bank in a window belongs to the entry's name"*.
|