60 lines
4.5 KiB
Markdown
60 lines
4.5 KiB
Markdown
# Subtitles (`RDialogueEvent` objects in the packages)
|
|
|
|
There are no subtitle files: every speech line has a dialogue object (class `RDialogueEvent`, Rocksteady) inside
|
|
the UE3 packages (`upk.md`), holding the subtitle text of all 11 languages. ackit reads them with
|
|
`ackit/subtitles.py` and writes them through `Workspace.texts` (`ackit/texts.py`).
|
|
|
|
## Inventory (buildid 3718300)
|
|
- 14,743 dialogue objects in the packages for 9,016 speech lines (a line's object is copied into every package
|
|
that uses it; all copies are identical), plus 41 scripted subtitles (below): ~9,400 subtitle strings and
|
|
168,688 stored copies.
|
|
- 11 languages: INT, FRA, ITA, DEU, ESN, ESM, POL, POR, RUS, JPN, KOR (the speech WADs exist only for INT, FRA,
|
|
ITA, DEU, ESN; the other languages play the INT voices with their own subtitles).
|
|
- ~9,400 subtitle strings per language: 94 % of the subtitles have one line, the rest up to 5 lines.
|
|
- Strings are stored as cp1252 (positive FString length) or UTF-16 (negative length): RUS, POL, JPN, KOR
|
|
and some INT/ESN/FRA/DEU strings are UTF-16.
|
|
|
|
## Scripted subtitles (`RSubtitle`)
|
|
41 more subtitles (radio and encryption-card lines) are `RSubtitle` objects in `Startup.upk`: the same layout,
|
|
with the per-language array named `ArraySubtitles` and a float (duration) at `0x2C` instead of a speech
|
|
reference. `RSubtitleLookup` (`Startup.upk`, `Lookups`) is an array of `{int key @0, object @4}`, key =
|
|
FNV-1 of the speech line name without `Speech_` (e.g. `CH_ZERO_CH0_ENCRYPTION_CARD2_01`), which links each
|
|
`RSubtitle` exactly to its line. ackit reads and writes both kinds the same way.
|
|
|
|
## Object layout (compact tagged properties)
|
|
The export data is a list of properties `u16 type, u16 field offset, value`, ended by `u16 0`. Types seen:
|
|
`0x10` object reference (i32 export/import index), `0x0D` FString, `0x09` struct/array (name i32+i32 = struct
|
|
or property name from the name table, i32 byte size, i32 array index, then `size` bytes), `0x04` 4-byte value,
|
|
`0x03` bool, `0x01` enum. Relevant properties:
|
|
|
|
| field | content |
|
|
|-------|---------|
|
|
| `0x10` @ `0x2C` | the speech event this line plays, e.g. export `Speech_CHURCH_B1_Ch2_POST_Explosion_BM_Cash_04` |
|
|
| `0x0D` @ `0x34` | speaker, e.g. `Batman` (`ackit verify --catalog` compares it with the curated speaker labels) |
|
|
| `0x09 LocalizedSubtitles` | u32 count, then per language: `0D 00 00 00` FString language code, `09 00 0C 00 Subtitles` struct, `00 00` |
|
|
| `Subtitles` | u32 line count, then per line: `0D 00 00 00` FString text, `04 00 0C 00` 4-byte value, `00 00` |
|
|
|
|
The sizes add up exactly in all 14,743 objects (checked by the parser: `SubtitleError` otherwise).
|
|
|
|
## Link to the speech sound
|
|
The speech event name is hashed (FNV-1) to the Wwise event ID and followed through the soundbanks (event ->
|
|
action -> containers -> Sound objects, the same in every language) to that language's recording of the line
|
|
(`banks.md`, *Lines*): a WAD variation or a localized in-memory sound. This is exact; the `_NN` suffix of the
|
|
event name does **not** match the variation order inside the WAD entry (WEMs are stored in media-ID order).
|
|
The names come from the packages' name and export tables (numbered names like `Speech_X_04` exist only as
|
|
export names) and from the subtitle objects themselves: some events are listed in no name table, but their
|
|
dialogue / RSubtitle object names them (Vicki Vale's radio reports, the Hush tape reveal).
|
|
An event that plays two versions of a line (105 subtitled events, e.g. Mr. Freeze's lines) links its subtitle
|
|
to both (`sound` = `key1;key2`). Linked: 103,304 of 103,658 strings in the 11 languages (INT 9,373 / 9,404),
|
|
each to the same line in every language. The rest (29 speech names, ~32 strings per language: Riddler trophy
|
|
texts, Arkham City Stories, radio reports and tapes the game does not ship audio events for) have no event in
|
|
any bank and stay editable without a sound link.
|
|
|
|
## How ackit writes them
|
|
- In place, into **every copy** (`data/catalog/SUB_locations.csv.gz`): the new text replaces the FString's
|
|
bytes and is padded with NUL characters, keeping the length field. The game stops at the first NUL, so no
|
|
size field, export or package layout changes. Limit: the new text must fit the original string's bytes
|
|
(cp1252: one byte per character; UTF-16: two) - shown as "room" in the CLI and the editor.
|
|
- A cp1252 string can only hold cp1252 characters; UTF-16 strings can hold any character.
|
|
- Packages are rebuilt from the store's original copy + all applied sound and text edits (`docs/decisions.md`).
|