Safety - Writes are serialized (Workspace.lock): two background jobs rebuilding one package (a sound and its subtitle applied side by side) lost an edit. - A store copies a bank file or package as "original" only if it matches the shipped catalog (Workspace.unoriginal_files: in-memory sounds, bank copies of voice lines, subtitles); the editor refuses to switch stores while changes are applied. A second store could otherwise resurrect a reverted sound. - Mod import in the editor asks before overwriting files changed outside the tool (it forced silently). - Revert all in the editor reverts texts too; the uninstaller counts text edits before offering to delete the store. - ackit restore-backup: the full-backup restore decisions.md described; it keeps the sounds of WADs without a full copy recorded. - build_catalog also stops when a subtitle differs from the catalog. Languages and texts - Game.language() returns languages without voices (JPN, RUS, ...) and Game.voice_archive() their INT voices; ackit text --lang JPN works. - Menu lookups parse one cached file (find & replace: minutes -> ms). - Subtitles decode cp1252 as Windows does: the UTF-8-in-cp1252 line Speech_TAPE_J_Answer_1_00 (KOR/POL/POR/RUS) is written back byte-for-byte; SUB_KOR/SUB_RUS rebuilt from the original game (only those rows changed). - Mods exported while another language is loaded keep name and line. Other fixes - Down-mix folds extra channels in at -3 dB (a 5.1 centre voice was dropped). - e2e_editor no longer overwrites the user's settings.json; kill_game waits for the game to exit before reverts. - Recorder handles Int32/UInt8 input and split frames; package block cache locked; conflict messages, CLI help examples, stale docstrings and doc references fixed; tests no longer read this PC's game language. Docs (decisions, user guide, README, subtitles.md, banks.md, catalog README, REPORT, NOTES, CLAUDE.md) updated; check_docs: 102 claims match. 148 tests; e2e_features 9/9, e2e_editor and verify PASS; game original afterwards.
65 lines
4.9 KiB
Markdown
65 lines
4.9 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,744 dialogue objects in the packages for 9,032 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,035-9,667 subtitle strings per language: 94-98 % of the subtitles have one line (JPN: all), the rest up
|
|
to 5 lines.
|
|
- Strings are stored as cp1252 (positive FString length) or UTF-16 (negative length): almost all RUS, JPN and
|
|
KOR strings and 85 % of POL are UTF-16; the other languages have 17-56 UTF-16 strings each.
|
|
4 cp1252 strings hold UTF-8 bytes instead (the same line in KOR, POL, POR, RUS: `Speech_TAPE_J_Answer_1_00`;
|
|
the KOR one with a damaged byte; the game's own data). ackit reads cp1252 as Windows maps it (every byte, also
|
|
the 5 that Python's cp1252 leaves undefined, which the RUS and KOR ones contain), so they are shown as stored
|
|
and every string is written back byte-for-byte.
|
|
|
|
## 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,744 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 (24 speech names, 24-36 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`).
|