Version 3.1.0: voice lines linked across all languages from the game data
This commit is contained in:
@@ -3,11 +3,11 @@
|
||||
| document | content |
|
||||
|----------|---------|
|
||||
| [user-guide.md](user-guide.md) | installing and using Unheard City (screenshots) |
|
||||
| [decisions.md](decisions.md) | stack, dependencies/licenses, originals store, size rule, trade-offs |
|
||||
| [decisions.md](decisions.md) | stack, dependencies/licenses, originals store, size rule, in-memory sounds, texts, language, verification |
|
||||
| [categories.md](categories.md) | group/category scheme and character fields |
|
||||
| [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/banks.md](formats/banks.md) | soundbanks and the in-memory sounds (footsteps, foley, effects, localized speech); how each voice line is linked across languages |
|
||||
| [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 |
|
||||
|
||||
@@ -50,10 +50,12 @@ list; unnamed IDs show as hex). First matching rule wins:
|
||||
BANK sounds (in-memory, `docs/formats/banks.md`) are classified by the name of their **soundbank** (e.g.
|
||||
`CHR_CatwomanFoleySuit`, `Speech_EMOTE_Batman`), which is more systematic than their event names.
|
||||
|
||||
Counts in the ITA catalog: Story missions 1993, Predator chatter 1796, Character lines 1537, Open-world chatter
|
||||
1295, Side missions 808, Combat barks 801, Efforts & emotes 616, Interview tapes 514, Riddler 360, Radio & TV 226,
|
||||
Gameplay & hints 199, DLC 167, Boss fights 150, Cinematics 150, Reactions 108, Interrogations 95. SFX archive:
|
||||
Music 389, Ambience 225, SFX 31, Unknown 6.
|
||||
Counts per voice language (identical in all 5): Story missions 1996, Predator chatter 1796, Character lines 1537,
|
||||
Open-world chatter 1295, Side missions 808, Combat barks 801, Efforts & emotes 616, Interview tapes 514,
|
||||
Riddler 360, Radio & TV 223, Gameplay & hints 199, DLC 167, Boss fights 150, Cinematics 150, Reactions 108,
|
||||
Interrogations 95. SFX archive: Music 389, Ambience 225, SFX 31, Unknown 6. BANK (all languages): Combat barks
|
||||
4360, Character foley 3051, Other speech 2590, Footsteps 1256, Ambient emitters 705, Weapons 494, Combat 412,
|
||||
Objects 401, ...
|
||||
|
||||
## Characters
|
||||
Two separate fields, because Wwise names describe a *scene*, not the speaker of each variation (a conversation
|
||||
@@ -61,10 +63,14 @@ alternates speakers between `NAME_1`, `NAME_2`, ...):
|
||||
|
||||
- **character** - the verified speaker, only from the curated lists in `data/catalog/source/<lang>/*.txt`
|
||||
(one file per character, listening-based). The lists name ITA sounds by their position in the ITA WAD
|
||||
(wemscan names); positions differ between languages, so each label is moved to the game's line name of that
|
||||
sound (`characters.csv`, empty `language`) and then applies to the same line in every language. Labels of
|
||||
sounds no event plays stay ITA-only (`language` = ITA). Checked against the speaker field of the game's
|
||||
dialogue objects: 955 / 955 labelled lines agree (spelling aside: TwoFace, Mysterious_stranger = Azrael).
|
||||
(wemscan names); positions differ between languages, so each listed recording is found at its ITA position
|
||||
and the label goes to its line (`characters.csv`, keyed by the line's name), i.e. to the same recording in
|
||||
every language. A label applies to exactly the listed recordings (not to other versions of the same event).
|
||||
Checked by `ackit verify --catalog` against the speaker field of the game's dialogue objects: 2,766 labelled
|
||||
lines agree (spelling aside: Mister Freeze, Bruce Wayne, TwoFace, Mysterious_stranger = Azrael), 25 listed
|
||||
as Unknown are named there, 19 differ (to listen to: `Batman_sword_raised_01-04` game: Ra's al Ghul,
|
||||
`ENCRYPTION_CARD2_02` game: Hugo Strange, `thug1_heckle_01_3` game: Thug 01, `SIDE_CAT_TF_intro1_03` game:
|
||||
Thug 06; the 12 Wonder City lines say "Announcement").
|
||||
Display names:
|
||||
Calendar -> Calendar Man, Croc -> Killer Croc, Freeze -> Mr. Freeze, Ivy -> Poison Ivy, Madhatter -> Mad
|
||||
Hatter, Quinn -> Harley Quinn, Ras -> Ra's al Ghul, Strange -> Hugo Strange, Talia -> Talia al Ghul,
|
||||
@@ -76,5 +82,5 @@ alternates speakers between `NAME_1`, `NAME_2`, ...):
|
||||
never shown as the speaker. In the editor the character filter matches verified speakers, and optionally
|
||||
inferred ones.
|
||||
|
||||
Coverage (every language): 3,366 sounds with a verified speaker, 5,930 more with inferred characters, 1,520 with
|
||||
neither (mostly predator/efforts from unnamed thugs).
|
||||
Coverage (every voice language, the same lines): 3,366 with a verified speaker, 6,263 more with inferred
|
||||
characters, 1,186 with neither (mostly predator chatter and efforts of unnamed thugs).
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## 1. Stack: Python + Qt (PySide6), packaged with PyInstaller
|
||||
- **Reuse:** the toolkit (`ackit`) is Python and proven against the real game; the editor imports it unchanged.
|
||||
- **Cross-platform GUI:** Qt Widgets via PySide6 (LGPL-3.0, official Qt for Python) looks native on
|
||||
Windows and Linux, has a fast model/view table for 11,000+ rows, and runs offscreen for automated GUI tests.
|
||||
Windows and Linux, has a fast model/view table for 20,000+ rows, and runs offscreen for automated GUI tests.
|
||||
- **Audio I/O:** Qt Multimedia (`QMediaDevices`, `QAudioSink`, `QAudioSource`) for playback, recording and
|
||||
device selection. It ships inside the PySide6 wheels with its own backends (WASAPI on Windows,
|
||||
PulseAudio/PipeWire on Linux), so unlike PortAudio/`sounddevice` nothing has to be installed on Linux.
|
||||
@@ -42,6 +42,7 @@ loopback); it is a developer tool and not part of the editor.
|
||||
every record. Stream-pack padding bytes are stored so pack reverts are byte-identical.
|
||||
- **Full-file fallback:** before the first change to a WAD (and its pack index), one verified full copy is kept
|
||||
in the store (`full/`), default on, can be disabled. Used by "Restore from full backup" if the store is damaged.
|
||||
Soundbank files, packages and localization files always get this copy (they are rebuilt from it, 4b/4c).
|
||||
- **Why the user data folder:** it survives app updates and reinstalls, needs no write access next to the game,
|
||||
and works the same on Linux/Proton. Steam never touches it.
|
||||
- **Safe writes:** every WAD is rebuilt into a temp file and atomically renamed over the original, pack indexes
|
||||
@@ -53,7 +54,8 @@ loopback); it is a developer tool and not part of the editor.
|
||||
|
||||
## 4. Size rule
|
||||
Speech/ambience variations are addressed by offset+size from BKPK tables inside compressed `.upk` packages,
|
||||
which ackit does not rewrite. Replacements are written in place and must not exceed the original WEM's size;
|
||||
which ackit leaves unchanged (it patches packages only in place). Replacements are written in place and must not
|
||||
exceed the original WEM's size;
|
||||
the encoder lowers quality, then sample rate, to fit, and refuses otherwise. Music stream packs have a loose
|
||||
index (`SFX\*.bnk`) that ackit rewrites, so music can be any length. In-memory sounds follow the same rule
|
||||
(below). Lifting the limit would mean re-laying out the uncompressed packages (export offsets, BKPK/DIDX
|
||||
@@ -76,8 +78,12 @@ tables): high risk for little gain, not done.
|
||||
- **Prefetch heads:** 396 speech lines (about 79 per language) have their first bytes copied into banks; a
|
||||
replacement of such a line rewrites those heads too, otherwise the start would play the old line.
|
||||
- **Names:** the hash list names none of these media. Names come from Wwise events whose names are in the
|
||||
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.
|
||||
packages' name and export tables (FNV-1 of the name = event ID), following event -> action -> containers ->
|
||||
sounds (6,456 sounds); the rest are numbered per bank. Categories use the bank name. A localized speech line
|
||||
has the same name in every language (its line, section 5).
|
||||
- **Localized speech:** speech banks hold one copy per voice language (4d); every media is labelled with the
|
||||
language(s) whose banks hold it (a few recordings are shared by several languages) and the catalog shows
|
||||
only the selected language's lines.
|
||||
|
||||
## 4c. Subtitles and menu text
|
||||
- **Subtitles** are written in place into every copy of the dialogue object, NUL-padded inside the original
|
||||
@@ -85,8 +91,11 @@ tables): high risk for little gain, not done.
|
||||
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.
|
||||
- **Links** subtitle -> speech sound come from the soundbanks (event -> ... -> per-language media -> WAD line
|
||||
or localized in-memory line), not from names: the `_NN` suffixes do not follow the WEM order inside WAD
|
||||
entries. Scripted subtitles (`RSubtitle`) are linked through the game's own lookup table (FNV-1 of the line
|
||||
name without `Speech_`). The subtitle objects' names also find events that no package name table lists, and
|
||||
an event that plays two versions of a line links its subtitle to both.
|
||||
- **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).
|
||||
@@ -103,10 +112,18 @@ copy per voice language) is filtered the same way, so the sound list never mixes
|
||||
- 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).
|
||||
- Verified speakers come only from the user's lists; name-based character inference is kept in a separate field.
|
||||
- Speech sounds are named after the game's line (speech event), not after their position in the WAD entry:
|
||||
the WEMs of an entry are stored in media-ID order and every language has its own media IDs, so position i
|
||||
is a different line in each language (75 % of the multi-variation lines). Line names make names, speaker
|
||||
labels, mod packages and subtitles agree across languages.
|
||||
- Voice lines are identified by the game's own data, not by names or positions (`docs/formats/banks.md`,
|
||||
*Lines*): the recordings that are sources of the same Wwise Sound objects are one line, with exactly one
|
||||
recording per language (12,434 lines, all 5 languages). The catalog stores that identity (`line`), and
|
||||
speaker labels, mod packages (another language: the same `line`) and subtitles follow it. Positions are
|
||||
useless across languages (WEMs are stored in media-ID order; position i is another line in 75 % of the
|
||||
multi-variation entries).
|
||||
- A line has one name in every language: the most specific speech event that plays it (10,156 of 10,815 voice
|
||||
lines), numbered `NAME#2` in the event's own order when it plays two versions; the other 659 keep the name of
|
||||
their English (INT) recording's position. The position in each language stays an alias (`NAME#i`).
|
||||
- A recording stored twice in a WAD (5 per language) is written and reverted in both places.
|
||||
- The catalog records the ORIGINAL bytes: `tools/build_catalog.py` stops before writing anything if a sound
|
||||
differs from the current catalog (a modified game), unless `--accept-changes` (a game update).
|
||||
- Category scheme: `docs/categories.md`.
|
||||
|
||||
## 6. Repository cleanup choices
|
||||
@@ -123,7 +140,9 @@ copy per voice language) is filtered the same way, so the sound list never mixes
|
||||
store does not need it.
|
||||
|
||||
## 7. Editor UX choices
|
||||
- One window: filters (left), sound table (top), editor of the selected sound (bottom). The primary flow
|
||||
- One window with three tabs. *Sounds*: filters (left) and sound table on top, the editor of the selected
|
||||
sound below across the full width, in three resizable columns (Listen | Replace | Subtitle) so nothing has
|
||||
to scroll at usual window sizes. *Subtitles* and *Menu text*: search, filters, table, editor. The primary flow
|
||||
(search -> select -> Space to listen -> Load file/Record -> Apply) needs no menus; power features (tags,
|
||||
favorites, multi-select revert, mod packages, settings) are in the toolbar and context menu.
|
||||
- Long work (loading, encoding, writing 0.5 GB WADs) runs in background jobs with status messages; the
|
||||
@@ -143,3 +162,18 @@ copy per voice language) is filtered the same way, so the sound list never mixes
|
||||
replacement file, decoded off the GUI thread), next to the Apply/Revert buttons while encoding/writing, and in
|
||||
the status bar for game loading and bulk changes.
|
||||
All filter selectors (including Character) are plain, non-editable drop-downs so they look and behave alike.
|
||||
|
||||
## 8. Verification
|
||||
- `pytest`: unit tests, a synthetic game (WADs, packs, packages with banks, localized banks, dialogue and subtitle
|
||||
objects, localization files), a two-language game built like the real one (per-language BKPK tables, a media
|
||||
ID shared by both languages, a recording stored twice), offscreen GUI tests, and read-only real-game tests
|
||||
when the game is installed.
|
||||
- `ackit verify` (WEMs rebuilt byte-for-byte, encode/decode), `ackit verify --catalog` (catalog vs game files,
|
||||
and every relation re-derived from the game files with independent checks; it prints them as facts).
|
||||
- Real-game write round trips without launching the game: `tools/e2e_features.py [--lang all]` (every copy of
|
||||
an in-memory sound, localized line, prefetch heads, subtitles incl. RSubtitle, menu text, sound + subtitle in
|
||||
one package, a mod moved to another voice language; `all` = each of the 11 languages) and
|
||||
`tools/e2e_editor.py` (through the editor's code path).
|
||||
- In the running game: `ackit verify --ingame` (music heard), `--banks` (patched packages loaded, probe data in
|
||||
memory), `--texts` (menu texts and Startup subtitles found in memory). Every tool reverts in a `finally` block
|
||||
and checks that every touched file is byte-identical afterwards.
|
||||
|
||||
@@ -7,10 +7,12 @@ Survey of the installed game (buildid 3718300), 2026-10-09.
|
||||
|
||||
## Inventory
|
||||
|
||||
| where | banks | unique in-memory WEMs | in the catalog |
|
||||
|-------|-------|-----------------------|----------------|
|
||||
| loose `SFX\*.bnk` (Batman/Catwoman footsteps, 32 surfaces each, + `TST_Prep_Media`) | 65 | 1,117 | 0 |
|
||||
| banks embedded in `.upk` packages (in 1,432 of the 2,650 packages) | 3,340 | 15,641 (~276 MB) | 0 |
|
||||
| where | banks | unique in-memory WEMs |
|
||||
|-------|-------|-----------------------|
|
||||
| loose `SFX\*.bnk` (Batman/Catwoman footsteps, 32 surfaces each, + `TST_Prep_Media`) | 65 | 1,117 |
|
||||
| banks embedded in `.upk` packages (in 1,432 of the 2,650 packages) | 3,340 | 15,641 (~276 MB) |
|
||||
|
||||
All of them are in the catalog (archive `BANK`).
|
||||
|
||||
The two sets share 119 media IDs: 15,800 unique media IDs in 96,011 places (bank copies). 396 of them are
|
||||
**prefetch heads** of streamed WAD sounds (below), so there are **15,404 in-memory sounds**. All copies of a
|
||||
@@ -52,11 +54,37 @@ WAD sounds have such heads (2,101 copies); replacing one of them must rewrite th
|
||||
|
||||
## Localized banks
|
||||
Speech banks are localized inside the (language-neutral) packages: one `AkBank` export starts with a language
|
||||
list (FStrings `INT FRA ITA DEU ESN`) and then holds one complete bank per language, in that order, all with
|
||||
the same bank ID; each language's copy has its own media IDs (checked on 215 banks against the WAD languages
|
||||
of their streamed lines). ackit labels every in-memory media with its language (`BANK.csv` column `language`):
|
||||
8,098 language-neutral sounds, ~1,460 per voice language. A catalog shows the neutral sounds plus the loaded
|
||||
language's lines; names are unique per language.
|
||||
list (FStrings `INT FRA ITA DEU ESN`) and then holds one block per language, in that order: the BKPK table(s)
|
||||
of the speech WAD entry for that language (`wad.md`) and a complete bank, with the same bank ID in every
|
||||
language. ackit gives each bank and each BKPK table the language of its position in that list (an export
|
||||
whose block count differs from its language count would be reported; none does). Each in-memory media gets
|
||||
the languages of the banks that hold it (`BANK.csv` column `language`): 8,098 sounds are language-neutral,
|
||||
~1,460 belong to one voice language, 6 to several (a DLC intro line shared by INT/FRA/ITA/ESN while DEU has
|
||||
its own); a media stored in the banks of every language is listed as `''` (the same recording everywhere).
|
||||
A catalog shows the neutral sounds plus the loaded language's lines; names are unique per language.
|
||||
|
||||
## Lines: one recording per language
|
||||
Implemented in `ackit/lines.py`, checked by `ackit verify --catalog`. A voice line exists once per language,
|
||||
and the game ties the recordings together itself:
|
||||
- Every WEM of a speech WAD has exactly one row in its own language's BKPK table (media ID, offset, size):
|
||||
10,816 of 10,816 in each of the 5 languages, no row without a WEM. 657 media IDs are used by several
|
||||
languages (cinematics, emotes, Clayface), each at another position in each language's WAD, so the tables
|
||||
are kept per language and never merged.
|
||||
- Every recording, streamed, prefetched or in memory, is the source of a Wwise Sound object; Sound objects,
|
||||
events, actions and containers have the same IDs in every language, only the media differ. A *line* = the
|
||||
recordings that are sources of the same Sound objects: 12,434 lines (WAD and in-memory), each with exactly
|
||||
one recording in each of the 5 voice languages (61,510 recordings), no Sound object with two recordings in
|
||||
one language. Line identity (`line` column) = the smallest Sound object ID.
|
||||
- Prefetched sources count too: the 396 lines with a prefetch head (mostly the game-over taunts) are linked
|
||||
only through them.
|
||||
- 5 recordings per language are stored twice in that language's WAD (same media ID in two entries, byte-
|
||||
identical, e.g. `Speech_STEEL_A1_CH2_Thug_behinddoormuffled` and `DIA_SteelA1Muffled`): the second place
|
||||
is a copy of the line (same `line`, keeps its position name) and is written and reverted with it, because
|
||||
the game plays whichever place the loaded package's table names.
|
||||
- Independent checks (no BKPK involved): every WAD recording is named as a source by a Sound object of a bank
|
||||
of its own language (54,055 of 54,055); every stored prefetch head equals the start of the recording its
|
||||
line gives it (2,101 of 2,101); a line's recordings have similar lengths across languages (11,761 of 12,434
|
||||
within a factor 1.5; the others are different edits, e.g. a 0.55 s ESN line vs 5.46 s elsewhere).
|
||||
|
||||
## Banks inside packages
|
||||
- The packages are LZO-compressed UE3 packages (`upk.md`); ackit decompresses them itself and searches the
|
||||
|
||||
@@ -6,7 +6,8 @@ the UE3 packages (`upk.md`), holding the subtitle text of all 11 languages. acki
|
||||
|
||||
## 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).
|
||||
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.
|
||||
@@ -29,21 +30,25 @@ or property name from the name table, i32 byte size, i32 array index, then `size
|
||||
| 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` |
|
||||
| `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, 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). Lines kept in memory link to the localized BANK sound of their language. Linked: ~99 % of
|
||||
the strings (e.g. INT 9,265 / 9,379); the rest have no event that reaches a sound (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.
|
||||
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
|
||||
|
||||
@@ -22,6 +22,16 @@ All offsets in the summary (names, exports, ...) refer to the **uncompressed** p
|
||||
package = the summary without the chunk table, followed by the chunks' data at their UncompressedOffset
|
||||
(the first chunk starts right after the summary, at NameOffset).
|
||||
|
||||
## Name, import and export tables (uncompressed package, at NameOffset / ImportOffset / ExportOffset)
|
||||
- Name: FString (positive length: ANSI incl. NUL; negative: UTF-16) + u64 flags.
|
||||
- Import (28 bytes): ClassPackage (name index, number), ClassName (index, number), i32 OuterIndex,
|
||||
ObjectName (index, number).
|
||||
- Export: i32 ClassIndex, SuperIndex, OuterIndex, ObjectName (index, number), ArchetypeIndex, **one extra u32
|
||||
(licensee 101)**, u64 ObjectFlags, i32 SerialSize, SerialOffset, u32 ExportFlags, i32 count + count x i32
|
||||
(net objects), 16-byte guid, u32 package flags: 72 bytes when the net-object list is empty. Checked on every
|
||||
export of the game. A name with number n is shown as `NAME_<n-1>` (e.g. the speech event `..._04`): such full
|
||||
names exist only in the export table. `Package.tables()` returns names, imports and exports.
|
||||
|
||||
## Compressed chunks
|
||||
Each chunk (at CompressedOffset): `u32 tag, u32 block size (0x20000), u32 compressed sum, u32 uncompressed
|
||||
sum`, then one `{u32 compressed size, u32 uncompressed size}` per block, then the blocks. Every block is an
|
||||
|
||||
@@ -26,14 +26,19 @@ Rules:
|
||||
Payload kinds:
|
||||
- **LOC WADs**: each entry is a group of WEMs, the variations of one line (ITA: 10816 WEMs in
|
||||
2464 entries; e.g. `CHR_RadioScanner` holds 91). Each WEM starts at the next 0x800 boundary after
|
||||
the previous one (zero padding); 3 entries pack two WEMs back to back. Addressed `HASH:i`.
|
||||
the previous one (zero padding); 3 entries pack two WEMs back to back. Addressed `HASH:i`. The WEMs of an
|
||||
entry are stored in media-ID order and the languages have different media IDs, so variation `i` is a
|
||||
different line in each language (75 % of the lines). Which recording is which line comes from the banks
|
||||
(`banks.md`, *Lines*), not from positions.
|
||||
- **SFX WAD**: 79 grouped ambience/effect entries (260 WEMs) plus 4 **stream packs** (below).
|
||||
|
||||
Where the game gets the inner offsets: the entry IDs appear next to `BKPK` headers inside the
|
||||
LZO-compressed UE3 packages (e.g. `BmGame.upk`, `OW.upk`, `Church_B1_*.upk`). Those embedded BKPK
|
||||
tables give each WEM's offset **and size**. ackit does not rewrite `.upk` files, so these WEMs are
|
||||
replaced **in place, no larger than the original** (zero-filled to the original size), and the WAD
|
||||
layout never changes. The user's earlier quickbms-reimport mod did the same and was loaded by the game.
|
||||
LZO-compressed UE3 packages (e.g. `BmGame.upk`, `OW.upk`, `Church_B1_*.upk`). A speech entry has one table
|
||||
per language, inside a localized `AkBank` export (its k-th table belongs to the k-th language the export lists;
|
||||
the SFX WAD's 79 tables are outside such exports). Those embedded BKPK tables give each WEM's offset **and
|
||||
size**; in every language each WEM has exactly one row (`banks.md`, *Lines*). ackit never changes them,
|
||||
so these WEMs are replaced **in place, no larger than the original** (zero-filled to the original size), and
|
||||
the WAD layout never changes. The user's earlier quickbms-reimport mod did the same and was loaded by the game.
|
||||
Only the 4 SFX stream packs, whose BKPK index is a loose `.bnk`, can change size.
|
||||
|
||||
## BKPK stream packs (`SFX\<ID>.bnk` + SFX WAD entry `<ID>`)
|
||||
@@ -61,9 +66,9 @@ end rounded up to 0x800. Padding is mostly zero (a few non-zero bytes in `EDF036
|
||||
original bytes where nothing moved).
|
||||
|
||||
Replacing a pack media (`ackit` target `EDF036D6/393BC53A`) rewrites the pack table (`.bnk`), the
|
||||
WAD entry payload, and the WAD table/offsets. Verified in-game (NOTES.md, E2).
|
||||
WAD entry payload, and the WAD table/offsets. Verified in-game (findings E2, E14).
|
||||
|
||||
## Regular soundbanks
|
||||
|
||||
The other `SFX\*.bnk` files and the banks embedded in `.upk` packages hold their WEMs in memory (not in a
|
||||
WAD); see `banks.md`. The toolkit does not read or modify them yet.
|
||||
WAD); ackit lists and writes them as the catalog archive `BANK` (`banks.md`).
|
||||
|
||||
@@ -70,8 +70,8 @@ Conclusion: approach A works end-to-end in-game. Use >= 30 s probes for the intr
|
||||
expects WAVEFORMATEX order (FL FR FC LFE BL BR) is unverified (identical-channel probes were used).
|
||||
16. (2026-10-09) The WADs hold only streamed audio. About 15,800 more sounds (footsteps, foley, combat, weapons,
|
||||
UI, grunts) are in-memory WEMs inside soundbanks: 65 loose `SFX\*.bnk` (1,117) and 3,340 banks embedded in
|
||||
the `.upk` packages (15,641; many banks copied into several packages). None are in the catalog; the WAD
|
||||
side is complete. Inventory, layout and editing consequences: `docs/formats/banks.md`.
|
||||
the `.upk` packages (15,641; many banks copied into several packages). None were in the catalog then (they
|
||||
are now: archive BANK); the WAD side was complete. Inventory and layout: `docs/formats/banks.md`.
|
||||
17. (2026-10-09) Speech lines have **per-language media IDs**: every package carries one BKPK table per language
|
||||
for the same WAD entry. 396 lines (78-80 per language) have a prefetch head (first N bytes) in banks; each
|
||||
head equals the start of exactly one language's WEM.
|
||||
@@ -95,12 +95,25 @@ Conclusion: approach A works end-to-end in-game. Use >= 30 s probes for the intr
|
||||
Position names (NAME_i, wemscan) therefore mean different lines per language. Speech sounds are now named
|
||||
after their speech event (same in every language); the curated ITA lists are mapped through it. Independent
|
||||
check: the speaker field of the dialogue objects agrees with the mapped labels for 955 / 955 lines.
|
||||
(Superseded by 22: the identity of a line now comes from the Sound objects, for every recording.)
|
||||
21. (2026-10-10) In-memory speech banks are localized (language list + one bank per language in one AkBank
|
||||
export, checked on 215 banks against the WAD languages of their streamed lines); 41 more subtitles are
|
||||
RSubtitle objects linked through RSubtitleLookup (key = FNV-1 of the line name without `Speech_`). Subtitle
|
||||
coverage by category is uneven in the game itself: efforts 0 %, combat barks 11 %, cinematics 10 % (cutscene
|
||||
lines are mostly not reached by speech events and have no dialogue object), story/predator/open world ~90 %+.
|
||||
|
||||
22. (2026-10-10) **One recording per language, from the game data alone** (`docs/formats/banks.md`, *Lines*).
|
||||
The BKPK tables of a speech entry sit inside the localized AkBank export, one per declared language; they
|
||||
used to be merged by media ID, but 657 media IDs are used by several languages at different positions, so
|
||||
the last language read (ESN) overwrote the others (120-157 WEMs per language looked unreferenced). Kept per
|
||||
language: 10,816 / 10,816 WEMs matched in every language. Prefetch sources were left out of the Sound-object
|
||||
map, which hid the 396 prefetched lines (game-over taunts). Five recordings per language are stored twice
|
||||
(same media ID in two entries, identical bytes). Subtitle objects name events no package lists (FNV-1 of the
|
||||
name = event ID: Vicki Vale's radio reports, Hush tape reveal). Result: 12,434 lines with exactly one
|
||||
recording in each of the 5 languages, no conflict; independent checks: 54,055 / 54,055 WAD recordings are
|
||||
sources in a bank of their own language, 2,101 / 2,101 prefetch heads equal the start of their recording,
|
||||
every linked subtitle is on the same line in all 11 languages.
|
||||
|
||||
## Old path -> new path
|
||||
`scripts/harness.py` -> `ackit verify --ingame` (ackit/verify/ingame.py); `scripts/validate_builder.py` ->
|
||||
`ackit/verify/selfcheck.py` + tests/test_realgame.py; `ackit/gamefiles.py` -> `ackit/game.py` + `ackit/originals.py`;
|
||||
|
||||
@@ -27,10 +27,14 @@ game is set to (Steam > game properties > Language) and applies to everything yo
|
||||
Changing it reloads the lists; the next start is again in the game's language.
|
||||
|
||||
## Names
|
||||
Voice lines carry the game's own line names (e.g. `Speech_TAPE_JOKER_1_04`), the same in every language and the
|
||||
same as in the Subtitles tab. Inside a WAD entry the game stores the variations of a line in a different order
|
||||
in each language, so the position-based name is kept only as a second name (`Speech_TAPE_JOKER_1#4` = the 4th
|
||||
WEM of that entry in this language; search finds both).
|
||||
Every voice line has one name in every language, taken from the game itself (e.g. `Speech_TAPE_JOKER_1_04`, the
|
||||
same as in the Subtitles tab): the line `Speech_X` in Italian is the same line `Speech_X` in English, French,
|
||||
German and Spanish. Inside a WAD entry the game stores the variations of a line in a different order in each
|
||||
language, so the position-based name is kept only as a second name (`Speech_TAPE_JOKER_1#4` = the 4th WEM of
|
||||
that entry in this language; search finds both). When the game plays one of two versions of a line, the
|
||||
second is shown as `NAME#2`. About 6 % of the lines are named by no game event; they take the name of their
|
||||
English recording in every language. A few recordings are stored twice in the game files: both rows belong to
|
||||
the same line and are changed and reverted together.
|
||||
|
||||
## Find a sound
|
||||
- Type in the **search box** (Ctrl+F): words can be parts of the name, a category, a character, your own
|
||||
@@ -77,8 +81,9 @@ than the original, since the game may cut it off.
|
||||
- Right-click rows → **Revert selected**; toolbar **Revert all** puts every original back.
|
||||
- Originals are kept in your user data folder (`%APPDATA%\UnheardCity` on Windows,
|
||||
`~/.local/share/unheardcity` on Linux), so reverting works after updating or reinstalling the app too.
|
||||
Uninstalling keeps this folder; use **Revert all** before uninstalling if you want the game back to original. Before the first
|
||||
change to an audio archive, one full copy of it is also kept there as a fallback (Settings).
|
||||
Uninstalling keeps this folder; use **Revert all** before uninstalling if you want the game back to original.
|
||||
Before the first change to a game file, a full copy of it is kept there too: optional for the WAD archives
|
||||
(Settings), always for soundbanks, packages and text files (they are rebuilt from that copy).
|
||||
- If Steam "Verify integrity of game files" or an update undoes your changes, a bar offers **Re-apply my
|
||||
changes** or **Forget them**. Do not use "verify" to undo mods; use Revert all.
|
||||
|
||||
@@ -90,11 +95,13 @@ The **Subtitles** tab lists the subtitle of every speech line in the selected la
|
||||
shows *used / room* characters). Latin-1 subtitles accept Latin-1 characters only (accents are fine);
|
||||
Russian, Polish, Japanese and Korean subtitles accept any character.
|
||||
- **Go to the sound** opens the speech line in the Sounds tab. In the Sounds tab, the editor of a speech line
|
||||
shows its subtitle under the audio: edit both side by side and use **Apply subtitle**, or **Show in
|
||||
Subtitles** to jump back.
|
||||
shows its subtitle in the **Subtitle** column next to the audio: edit both side by side, **Apply** /
|
||||
**Revert** the subtitle there, or **Show in list** to jump back to the Subtitles tab.
|
||||
- **Find and replace…** works on the rows the filters show (texts that would not fit are listed and skipped).
|
||||
- **Revert** / **Revert all texts here** put the original text back (every copy in every game file).
|
||||
- About 8 % of the subtitles belong to lines the game never plays (placeholders); they have no linked sound.
|
||||
- About 30 subtitles per language have no sound in the game (texts such as the Arkham City Stories, unused
|
||||
radio reports); they have no linked sound. A subtitle of a line the game plays in two versions belongs to
|
||||
both. The game itself has no subtitles for efforts and grunts and for most combat barks and cutscene lines.
|
||||
|
||||
## Menu text
|
||||
The **Menu text** tab lists every menu, HUD, bio, riddle and credits string of the selected language (about
|
||||
@@ -107,10 +114,14 @@ Right-click → **Notes & tags…** (F2): free text and comma-separated tags, se
|
||||
Click the **★** column to mark favorites. These are stored in your user data folder, not in the game.
|
||||
|
||||
## Share your changes (mods)
|
||||
- **Export mod…**: choose which changed sounds to include and save a `.acmod` file (the exact game data plus
|
||||
the audio, so it also works for another language whose original has a different size).
|
||||
- **Export mod…**: choose which changed sounds and texts to include and save a `.acmod` file (the exact game
|
||||
data plus the audio, so it also works for another language whose original has a different size).
|
||||
- **Import mod…**: shows which sounds match this game, then installs them. You can revert them like any change.
|
||||
- Changed subtitles and menu texts are exported and imported with the sounds (listed as *Text*).
|
||||
- Changed subtitles and menu texts are exported and imported with the sounds (listed as *Text*); a text stays
|
||||
in its language (an Italian subtitle edit changes the Italian subtitle).
|
||||
- In another language, voice lines are matched by the game's own identity of the line, so the same line is
|
||||
replaced even where its position differs. Voice lines from packages made with versions before 3.1 carry no
|
||||
such identity: the import lists them instead of guessing (export the mod again with this version).
|
||||
|
||||
## Settings
|
||||

|
||||
@@ -125,6 +136,7 @@ The editor panel opens tall enough to show all its controls; drag the divider ab
|
||||
| "Cannot write … close the game" | quit Arkham City (and anything else using the file) and try again |
|
||||
| "not a Batman: Arkham City folder" | pick the folder that contains `BmGame` and `Binaries` |
|
||||
| "audio is too long for this sound" | shorten the audio (see the original's length) |
|
||||
| "the text is … characters long but this subtitle has room for …" | shorten the subtitle (the counter shows the room) |
|
||||
| "Cannot read … Supported: WAV, FLAC, …" | convert the file to WAV/MP3/FLAC, or install ffmpeg |
|
||||
| "changed outside this tool" | the game files were changed by an update or another tool; revert or confirm to overwrite |
|
||||
| no sound when playing | choose another playback device in Settings |
|
||||
|
||||
Reference in New Issue
Block a user