Files
UnheardCity/data/catalog/README.md
Cristian Gora 1ccaa8b398 Full correctness check against the docs: safer writes, reverts and texts
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.
2026-10-13 21:34:13 +02:00

6.6 KiB

Sound catalog

file content
<ARCHIVE>.csv every sound of one audio archive: INT, FRA, ITA, DEU, ESN (speech WADs, one per language), SFX (music, ambience and effects streamed from the SFX WAD) and BANK (in-memory sounds stored inside soundbanks: footsteps, foley, combat, weapons, UI, efforts; docs/formats/banks.md)
BANK_locations.csv.gz every stored copy of the BANK sounds, and the bank copies of WAD speech recordings (prefetch heads, whole copies; below)
SUB_<LANG>.csv subtitles of the speech lines, one file per language (11 languages; docs/formats/subtitles.md)
SUB_locations.csv.gz every stored copy of each subtitle string (dialogue objects in the packages)
characters.csv name,character,language: verified speaker per line name (empty language: every language, the same line everywhere; a language code would label a sound of that language only - none needs it now)
index.json per archive: fingerprint of the game files the CSV was built from (ackit/game.py: wad_fingerprint, ackit/banks.py: game_fingerprint) and counts
source/<lang>/*.txt the curated original lists (one file per character, one wemscan-style file name per line, e.g. Speech_TAPE_JOKER_1_4.wem). Files.txt = all files, Stats.txt = counts. Never edited by tools.

All CSVs are UTF-8 with a header row. They contain only names and metadata (no game audio).

<ARCHIVE>.csv columns

column meaning
archive INT, FRA, ..., SFX or BANK
target location: HASH (WAD entry with one WEM), HASH:i (i-th variation, 0-based), PACK/MEDIA (music stream pack), MEDIA (BANK: Wwise media ID)
name display name. Speech: the line's name, the same in every language (docs/decisions.md 5): its speech event (e.g. Speech_TAPE_JOKER_1_04, NAME#2 = second version an event plays) or, for lines no named event plays, the INT recording's position name. A recording stored twice keeps its position name in the second place. SFX: NAME, NAME_1..n. BANK: the Wwise event that plays it (Play_..._i) or <Bank>_<n> (<Bank>_<MEDIA> where that name is taken, e.g. by the SFX WAD's position names)
aliases other accepted names, |-separated: NAME#i = i-th WEM of the entry in this language's WAD (positions differ between languages), the entry name for single entries, <Bank>/<MEDIA> for BANK
group Speech, Music, Ambience, SFX, Unknown (docs/categories.md)
category finer category (docs/categories.md)
bank BANK: the soundbank holding the sound (first by name if several do); empty for WAD sounds
language BANK: language(s) of a localized speech line, ;-separated (INT, or ESN;FRA;INT;ITA for one recording shared by those); empty = the same sound in every language. The editor shows the sounds of the selected language; names are unique per language
line speech (WAD and BANK): the line's identity, the same in every language (smallest Wwise Sound object ID of the line; ackit/lines.py). Two rows of one archive with the same line are one recording stored twice (written together). Empty for non-speech sounds
character verified speaker(s) from characters.csv, ; -separated, empty if unknown
characters_inferred characters/roles mentioned by the name (and bank) (hint only)
seconds, rate, channels of the original sound
size size of the original WEM in bytes = maximum size of a replacement (except music packs: any size)
sha1 first 16 hex digits of the original WEM's SHA-1 (detects files changed outside the tool; empty when the copies of a BANK sound differ)
notes free text (empty; for future curation, e.g. transcripts)

BANK_locations.csv.gz columns (gzip-compressed CSV)

column meaning
media Wwise media ID (hex)
file file under BmGame/CookedPCConsole, /-separated (SFX/xxxxxxxx.bnk or Name.upk)
offset position of the WEM in the uncompressed file (hex; docs/formats/upk.md)
size bytes reserved for it there
kind m = copy of a BANK sound, p = copy of (the start of) a WAD speech recording: a prefetch head, or a whole copy with the same media ID
sound p rows: key of that speech recording (e.g. INT:2B151272:0); each language has its own recordings and heads

A replacement is written into every m copy of a BANK sound; replacing a speech line also rewrites its p copies (docs/formats/banks.md).

SUB_<LANG>.csv columns

column meaning
speech the speech event of the line (e.g. Speech_CHURCH_B1_Ch2_POST_Explosion_BM_Cash_04)
line 0-based line of a multi-line subtitle
text original subtitle text
room maximum length of a replacement in characters (it is written in place)
enc a = cp1252 string, w = UTF-16 string
sound key(s) of the linked voice recording(s), ;-separated when the event plays two versions: this language's WAD line or in-memory BANK: line (the INT ones for languages without voices); empty when no event reaches a sound

SUB_locations.csv.gz: lang, speech, line, file, pos (hex, uncompressed offset of the FString), size, enc. Menu text has no catalog: the localization files are read directly (docs/formats/text.md).

The editor uses these files directly when the installed game matches index.json; for another game build it scans the game itself and applies the same rules (no checksums then; the bank scan takes a few minutes). ackit verify --catalog cross-checks the catalog in use against the game files (--quick samples packages).

Rebuilding / adding a language

  1. Make sure the game files are original (ackit status; ackit revert --all).
  2. Optional: add listening-based lists for another language in source/<lang>/<Character>.txt (same format: wemscan names of THAT language's WAD). Each list is resolved against its own language's positions and moved to the lines, so a label then applies to the same recording in every language.
  3. Run python tools/build_catalog.py - it scans every archive and all soundbanks of the installed game (all languages present; ~5 minutes) and rewrites the CSVs, the location indexes, characters.csv and index.json. It stops before writing anything if a sound or a subtitle differs from the current catalog (a modified game); after a game update run it with --accept-changes.
  4. Run ackit verify --catalog (no issues expected). New character labels need a display name in ackit/classify.py: USER_LABELS (otherwise the file name is used).