Edit subtitles and menu text

This commit is contained in:
2026-10-09 22:36:46 +02:00
parent 5d6c2447f7
commit ef68edbe45
40 changed files with 105877 additions and 1291 deletions

View File

@@ -8,6 +8,8 @@
| [building.md](building.md) | building the editor and its installer (Windows, Linux) |
| [formats/wad.md](formats/wad.md) | WWAD archives, grouped entries, BKPK stream packs |
| [formats/banks.md](formats/banks.md) | soundbanks and the in-memory sounds not covered yet (footsteps, foley, effects) |
| [formats/subtitles.md](formats/subtitles.md) | subtitles: dialogue objects in the packages, link to the speech sounds |
| [formats/text.md](formats/text.md) | menu and UI text: the localization files |
| [formats/upk.md](formats/upk.md) | UE3 packages: summary, LZO chunks, how ackit reads and patches them |
| [formats/wem.md](formats/wem.md) | WEM layout the game accepts, setup stripping, hash and decode-alloc formulas |
| [reverse-engineering/decoder.md](reverse-engineering/decoder.md) | BatmanAC.exe decoder functions and addresses |

View File

@@ -79,6 +79,19 @@ tables): high risk for little gain, not done.
packages' name tables (FNV-1 of the name = event ID), following event -> action -> containers -> sounds
(6,416 sounds); the rest are numbered per bank. Categories use the bank name.
## 4c. Subtitles and menu text
- **Subtitles** are written in place into every copy of the dialogue object, NUL-padded inside the original
string (the game stops at the first NUL), so nothing in the package moves; the limit is the original
string's length ("room"). Moving the object to the end of the package (new export offset/size and an extra
chunk) would lift the limit, but no dialogue is loaded on the title screen, so the result could not be
checked automatically in the game: not done.
- **Links** subtitle -> speech sound come from the soundbanks (event -> ... -> per-language media -> WAD), not
from names: the `_NN` suffixes do not follow the WEM order inside WAD entries.
- **Menu text** files are plain UTF-16 text: no size limit; only the edited values change.
- Both use the originals store like sounds: texts are recorded as strings (`texts` in store.json) and every
file is rebuilt from its original copy + the applied edits (sound and text edits combine in one package).
- Keys (`SUB:<LANG>:<speech>:<line>`, `MENU:<LANG>:<File>:<Section>:<Key>`) are stable and used in mod packages.
## 5. Catalog
- Shipped CSVs per archive (names, categories, characters, original size + checksum) make loading instant and
enable change detection; any other build is scanned at runtime (header reads only, cached).

46
docs/formats/subtitles.md Normal file
View File

@@ -0,0 +1,46 @@
# 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).
- 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.
## 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` |
| `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, followed through the soundbanks (event ->
action -> containers -> Sound) to the streamed media, and that media ID is looked up in the per-language
BKPK tables (`banks.md`) to find the WAD entry and variation of each language. 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). Linked: ~92 % of the strings (e.g. INT 8,637 / 9,379); the rest have no event that reaches a
streamed media (unused or placeholder lines such as `..._TEMP_29`) and stay editable without a sound link.
Event names with a number (`Speech_X_04`) exist only as export names, not in the name table.
## 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`).

19
docs/formats/text.md Normal file
View File

@@ -0,0 +1,19 @@
# Menu and UI text (`BmGame\Localization\<LANG>\*.<lang>`)
The menus, HUD, character bios, riddles, credits and other UI strings are UE3 localization files, one folder
per language (INT, ITA, FRA, DEU, ESN, ESM, POL, POR, RUS, JPN, KOR), ~37 files and ~7,000 strings each
(`FRONTEND`, `GFxUI`, `Bios`, `Credits`, `PC`, `RIDDLER`, ...). ackit reads and writes them with
`ackit/loctext.py` through `Workspace.texts`.
## Format
- UTF-16LE with a BOM (`FF FE`), CRLF line ends; the first line is empty. (`INT\Version.int` and
`VersionData.int` are plain ASCII build IDs, not UI text: ackit ignores them.)
- `[Section]` lines, `Key=Value` lines, `;` comments. A few values are in double quotes (`Quit="Quit game"`).
- File extension = language code (`FRONTEND.int`, `FRONTEND.ITA`); case varies, lookups are case-insensitive.
- Markers inside values (e.g. `\n`, button tags) are the game's own; ackit keeps them as plain text.
## Keys and writing
- Key: `MENU:<LANG>:<File>:<Section>:<Key>` (`#n` for a repeated key; none in the shipped files).
- Writing replaces only the value part of the edited lines (quotes kept); BOM, encoding, other lines and line
ends stay byte-identical. Values cannot contain line breaks; characters must fit the file's encoding.
- The files are rebuilt from the store's original copy + the applied edits, so reverting restores them exactly.