From 98ec2fe9c46188c5c3ee395c88111769b8b63809 Mon Sep 17 00:00:00 2001 From: Cristian Gora Date: Fri, 9 Oct 2026 22:50:15 +0200 Subject: [PATCH] In-game text test, version 3.0.0, report --- CLAUDE.md | 2 +- NOTES.md | 6 +- README.md | 2 +- REPORT.md | 87 +++++++++++++++++----------- ackit/__init__.py | 2 +- ackit/cli.py | 3 + ackit/verify/ingame.py | 51 ++++++++++++++++ docs/building.md | 4 +- docs/reverse-engineering/findings.md | 4 ++ pyproject.toml | 2 +- 10 files changed, 123 insertions(+), 40 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d555fc9..976f2e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ Batman: Arkham City (PC) audio toolkit + editor. Read `NOTES.md` first (status, Texts go through `Workspace.texts` (subtitles in packages, in place and NUL-padded; menu `.int` files). - Never use Steam "verify integrity". Keep game files original after tests (`ackit status`, `ackit revert --all`). - Real-game checks: `ackit verify` (offline), `ackit verify --catalog [--quick]` (catalog vs game files), - `ackit verify --ingame [--banks]` (Windows; needs Steam running and logged in, WinDbg; ~1.5 min; reverts and + `ackit verify --ingame [--banks | --texts]` (Windows; needs Steam running and logged in, WinDbg; ~1.5 min; reverts and checks byte-identity itself). - Rebuild catalogs only from original game files: `python tools/build_catalog.py` (~5 min, scans all packages). - Shell: git at `C:\Program Files\Git\cmd` (prepend to PATH). Write Python with the Write/Edit tools; avoid diff --git a/NOTES.md b/NOTES.md index b4139f3..4ab9929 100644 --- a/NOTES.md +++ b/NOTES.md @@ -36,7 +36,11 @@ counter, apply/revert/revert all, find & replace on the filtered rows, go to sound); the sound editor shows and edits the sound's subtitle (SubtitleBox); texts in mod export/import and the changed-outside banner; smoke test loads texts. 15 GUI tests. - 6. [ ] Final e2e (audio + text), packaged build, REPORT.md. + 6. [x] Final pass 2026-10-09: `ackit verify` PASS, `--catalog` OK, `--ingame` PASS (ncc 1.0), `--ingame + --banks` PASS, `--ingame --texts` PASS (12/12 menu markers in memory), tools/e2e_editor.py PASS; game + original after. Version 3.0.0; dist/UnheardCity-3.0.0-windows-amd64.zip (86 MB) smoke + GUI start PASS + with PATH = C:\Windows. 121 tests. REPORT.md rewritten (unresolved items). +- NEXT (needs a person): play a mission with a replaced footstep/foley sound and an edited subtitle (REPORT.md). - OPEN: channel order for >2 channels unverified; speech and in-memory sounds *heard* in-game (need gameplay; posted keys do not drive the menu). diff --git a/README.md b/README.md index b2cc7ac..f17772e 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,4 @@ NOTES.md project status and working notes; CLAUDE.md: guidance for AI ses ``` Tests: `pytest` (the real-game tests are skipped when the game is not installed). -In-game test (Windows, Steam running, WinDbg installed): `ackit verify --ingame`. +In-game tests (Windows, Steam running, WinDbg installed): `ackit verify --ingame`, `--ingame --banks`, `--ingame --texts`. diff --git a/REPORT.md b/REPORT.md index 540d3c3..b35d1dd 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,38 +1,59 @@ -# Report — repository cleanup and the editor, now Unheard City (mission 2) +# Report — full-game audio, subtitles and menu text (mission 3) + +Brief: `docs/history/mission3_audio_text_prompt.md`. Earlier missions: the WAD/WEM toolkit (mission 1) and the +editor Unheard City (mission 2). ## What works (verified) -- **Repository**: clean layout (`ackit/`, `app/`, `tests/`, `data/catalog/`, `docs/`, `tools/`); old state in git - tag `pre-cleanup`. One CLI (`ackit`) and one app (`UnheardCity`). -- **Toolkit**: portable encoder (libvorbis via libsndfile; no Wwise, ffmpeg or vgmstream), WAD/pack injector, - originals store with byte-exact revert, conflict detection, mod packages, Steam/Linux/Proton game discovery. -- **Catalog**: CSVs for ITA/INT/FRA/DEU/ESN/SFX (11,467 sounds per speech language set), categories, verified - speakers from the user's lists (3,366 lines in every language), inferred characters for the rest, original - sizes and checksums. Schema in `data/catalog/README.md`. -- **Editor**: browse/search/filter, listen (A/B, loop, waveforms), replace from file or recording, trim/gain/ - normalize/loudness, apply/revert/revert all, changed-underneath warnings, notes/tags/favorites, mod - export/import, settings. -- **Tests**: 75 automated tests (synthetic game, real-game read-only tests, offscreen GUI tests) pass. - `ackit verify` (offline self-check on the installed game) PASS. -- **Real game**: `ackit verify --ingame` PASS (ncc 1.0); `tools/e2e_editor.py` — replacement applied through the - editor is heard in the game (ncc 1.0, no crash), revert leaves WAD and pack index byte-identical; a speech line - applied and reverted byte-identically. Game files are original at the end. -- **Package (Windows x86-64)**: `dist/UnheardCity-2.0.0-windows-amd64.zip` (78 MB) with - `THIRD_PARTY_LICENSES.txt`; runs its smoke test and starts the GUI with `PATH` reduced to `C:\Windows`. - Contains a per-user installer (`Install.cmd`): install, upgrade and uninstall (Settings > Apps) verified. +- **Every sound of the game is cataloged and editable**: the speech WADs (5 languages x 10,816), the SFX WAD (651) + and, new, the **15,404 in-memory sounds** stored inside soundbanks (archive `BANK`: footsteps, foley, combat, + weapons, UI, efforts) in loose `.bnk` files and in 1,499 `.upk` packages. ackit reads and patches the + LZO-compressed UE3 packages itself (`ackit/upk.py`, `lzo.py`; no external tools). A replacement goes into every + copy (93,910 copies); 396 speech lines with prefetch heads in banks get their heads rewritten too. + Names: 6,456 BANK sounds are named after their Wwise event (`Play_...`), the rest after their bank. +- **Catalog check**: `ackit verify --catalog` compares the catalog with a fresh scan of the game (missing, extra, + duplicate, size/checksum, category, character, language, bank copies, subtitle copies/texts): **catalog OK** + on the full game. It found and fixed 2 duplicate names per language (variation shown as `NAME#i`). +- **Subtitles**: found in `RDialogueEvent` objects inside the packages (9,016 speech lines x 11 languages, + 168,122 stored copies), parsed, cataloged (`SUB_.csv`) and linked to their speech sound through the + soundbanks (92 % of the strings; the rest are unused/placeholder lines). Edited in place in every copy. +- **Menu text**: the localization files (`BmGame\Localization\\*.int`, ~7,000 strings per language), + edited value by value; everything else in the files stays byte-identical. +- **Editor**: tabs *Sounds* / *Subtitles* / *Menu text*. The sound editor shows and edits the selected speech + line's subtitle side by side; the subtitle list links back to the sound. Search, filters (character, + category, file, modified), room counter, apply/revert/revert all, find & replace on the filtered rows, mod + export/import with texts, changed-outside banner for sounds and texts. +- **Safety**: every changed file (WAD, bank, package, localization file) is rebuilt from a verified original copy + in the store + the edits applied now; reverting the last edit restores it byte-for-byte; files changed by + another tool or Steam are detected (whole-file SHA-1) and not overwritten without confirmation. +- **Tests**: 121 automated tests (unit, synthetic game with packages/banks/dialogues/localization, real-game + read-only, offscreen GUI) pass. `ackit verify` PASS (incl. 300/300 in-memory WEMs rebuilt byte-identically). +- **Real game** (2026-10-09), all reverting and checking byte-identity themselves: + `ackit verify --ingame` PASS (music heard, ncc 1.0); `--banks` PASS (77 in-memory sounds of the front-end + packages replaced in 54 files: the game loads them, no crash); `--texts` PASS (12 menu texts found in the + game's memory, 12 subtitles patched in packages, no crash); `tools/e2e_editor.py` PASS. Game files original + at the end (`ackit status`). +- **Package**: `dist/UnheardCity-3.0.0-windows-amd64.zip` (86 MB, catalogs included). With `PATH` reduced to + the Windows folders the packaged exe passes its smoke test (26,871 sounds loaded, encode + inject, 9,352 + subtitles and 7,020 menu texts loaded, an in-memory sound decoded from a package) and the GUI starts and + quits cleanly. The installer is unchanged since mission 2 (install/upgrade/uninstall verified then). -## What is not verified / limitations -- **Coverage**: only sounds in the WAD archives are listed and editable. About 15,800 short effects (footsteps, - foley, weapons, UI, grunts) are stored inside soundbanks (loose `.bnk` and banks embedded in `.upk` - packages) and are not covered yet (`docs/formats/banks.md`). -- Speech lines being *heard* in the game needs gameplay; verified indirectly (same decoder/WAD path as the music - test, byte-level apply/revert, boot test with modified speech WAD). -- Speech/effects replacements must fit the original's size (index lives in compressed `.upk` packages). -- Channel order for 4-6 channel sounds is unverified (tests used identical channels). -- Inputs: WAV/FLAC/OGG/OPUS/MP3/AIFF built in; M4A/AAC/WMA only with an ffmpeg installed on the system. -- Character labels outside the user's lists are inferred from names (marked as such), not verified speakers. +## Unresolved / limitations +- **Heard in game, not proven automatically**: in-memory sounds and subtitles. The game loads the patched + packages (probe data and texts found in its memory, no crash), but nothing of this plays/shows on the title + screen without input, and keys posted to the game window do not reach the menu (findings 18). Speech lines + were already in this situation; the decoder path is the same one proven for music. +- **Size limits**: in-memory sounds and speech must fit their original size; subtitles must fit the original + text's length ("room"); cp1252 subtitles accept cp1252 characters only. Lifting these limits means moving + objects inside the packages (new export offsets/sizes, added chunks): designed but not done, because it could + not be verified automatically in the game (decisions 4c). +- **Unlinked subtitles**: ~8 % of the subtitle strings have no event path to a streamed sound (unused or + placeholder lines); they are listed and editable but not linked. +- **Unnamed sounds**: ~9,000 BANK sounds have no event name and are named `_`. +- Two copies of media `1C898731` differ; its checksum is left empty. Channel order for 4-6 channel sounds is + unverified. M4A/AAC/WMA input needs an ffmpeg on the system. +- Another game build is scanned at first start (WADs: seconds; banks and subtitles: a few minutes, cached). ## Next steps -1. Support the in-memory sounds in soundbanks: the loose footstep banks first, then the banks embedded in - `.upk` packages (`docs/formats/banks.md`). -2. Extend the curated speaker lists (`data/catalog/source//*.txt`) and rebuild with `tools/build_catalog.py`. -3. Optional: rewrite the BKPK indexes inside `.upk` packages (LZO) to lift the size limit for speech. +1. Gameplay check (a person): a replaced footstep/foley sound and an edited subtitle in a mission. +2. Optional: lift the subtitle length limit by relocating dialogue objects (needs that gameplay check). +3. Extend the curated speaker lists (`data/catalog/source//*.txt`) and rebuild the catalog. diff --git a/ackit/__init__.py b/ackit/__init__.py index 1ec7a7d..03b21a4 100644 --- a/ackit/__init__.py +++ b/ackit/__init__.py @@ -8,4 +8,4 @@ Library layers (low -> high): inject (WAD writing), originals (store), workspace (apply/revert), modpack (share) tools cli (`ackit` command), verify/ (offline self-check, in-game test on Windows) """ -__version__ = "2.0.0" +__version__ = "3.0.0" diff --git a/ackit/cli.py b/ackit/cli.py index 983bc0c..2f23b0f 100644 --- a/ackit/cli.py +++ b/ackit/cli.py @@ -310,6 +310,8 @@ def cmd_verify(a): from .verify import ingame if a.banks: rep = ingame.run_banks(ws, a.sound or None) + elif a.texts: + rep = ingame.run_texts(ws) elif a.baseline: rep = ingame.run(ws, a.target, mode="baseline", seconds=a.seconds) elif a.installed: @@ -390,6 +392,7 @@ def build_parser(): s.add_argument("--banks", action="store_true", help="with --ingame: test in-memory sounds (default: the front-end banks, or --sound NAME ...)") s.add_argument("--sound", action="append", default=[], help="with --banks: a sound to test (repeatable)") + s.add_argument("--texts", action="store_true", help="with --ingame: test menu text and subtitle edits") # advanced s = add("encode", cmd_encode, "[advanced] audio -> .wem file", (enc,)) s.add_argument("audio"); s.add_argument("out"); s.add_argument("--like", metavar="NAME") diff --git a/ackit/verify/ingame.py b/ackit/verify/ingame.py index a121fa9..d6e2df3 100644 --- a/ackit/verify/ingame.py +++ b/ackit/verify/ingame.py @@ -121,6 +121,57 @@ def run(ws, key=DEFAULT_TARGET, wem=None, mode="modded", seconds=40.0, threshold return report +# --- texts -------------------------------------------------------------------------------------- +def run_texts(ws, seconds=45.0, menu_files=("FRONTEND", "GFXUI"), n_menu=12, n_subs=12, logdir=None, log=print): + """Write markers into menu texts of the game's language (front-end files, loaded at startup) and into some + subtitles, start the game, then check: no crash, the menu markers are in the game's memory (it loaded the + edited localization files), and after the revert every touched file is byte-identical. Subtitles are not + loaded on the title screen, so for them the check is that the patched packages do not break the game.""" + import random + from .debugger import Run, kill_game, search_memory + tx = ws.texts + lang = ws.game.language() + menu = [e for e in tx.menu(lang) if e.file.split(".")[0].upper() in menu_files and len(e.value) >= 4] + rnd = random.Random(3) + picks = rnd.sample(menu, min(n_menu, len(menu))) + changes = {"MENU:" + e.key: "ACKIT%02d %s" % (i, e.value) for i, e in enumerate(picks)} + subs = [s for s in tx.subtitles(lang) if s.room >= 12] if lang in tx.subtitle_languages() else [] + for i, s in enumerate(rnd.sample(subs, min(n_subs, len(subs)))): + changes["SUB:%s:%s:%d" % (lang, s.speech, s.line)] = ("AKS%02d " % i + s.text)[:s.room] + files = sorted({p for k in changes for p in tx.files(k)}) + logdir = logdir or os.path.join(os.getcwd(), "logs") + os.makedirs(logdir, exist_ok=True) + logp = os.path.join(logdir, "ingame_texts_%s.log" % time.strftime("%Y%m%d_%H%M%S")) + report = dict(mode="texts", lang=lang, menu_texts=len(picks), subtitles=len(changes) - len(picks), + files=len(files)) + kill_game() + before = {f: _sha1_file(f) for f in files} + tx.apply(changes, progress=log) + needles = {("ACKIT%02d " % i).encode("utf-16-le") for i in range(len(picks))} + runner = Run(ws.game.exe, logp) + found = set() + try: + report.update(runner.start()) + while time.time() - runner.t0 < seconds and runner.alive(): + time.sleep(0.5) + if re.search(r"^ACKIT_CRASH", runner.log(), re.M): + break + if runner.alive(): + found = search_memory(report["pid"], needles) + report["alive_at_end"] = runner.alive() + finally: + runner.stop() + tx.revert(list(changes), progress=log) + report["reverted_byte_identical"] = all(_sha1_file(f) == h for f, h in before.items()) + report["crash"] = bool(re.search(r"^ACKIT_CRASH", runner.log(), re.M)) + report["menu_markers_in_memory"] = len(found) + ok = (not report["crash"] and report["alive_at_end"] and report["menu_markers_in_memory"] > 0 + and report["reverted_byte_identical"]) + report["result"] = "PASS" if ok else "FAIL" + json.dump(report, open(logp.replace(".log", ".json"), "w"), indent=1) + return report + + # --- in-memory sounds (soundbanks inside the packages) ------------------------------------------ PROBE_RATE, PROBE_Q = 28000, 4 # a codebook setup the game's own WEMs do not use (E10) diff --git a/docs/building.md b/docs/building.md index 5b18d23..2cafb23 100644 --- a/docs/building.md +++ b/docs/building.md @@ -15,8 +15,8 @@ That one command does everything, offline. If `app/assets/` (committed icon and made from `bac_logo_full.png` and `wallpaper.jpg` in `Downloads\Arkham City` (or `--art FOLDER`); without those images the build still works, with a plain icon and no wallpaper. Output: `dist\UnheardCity\UnheardCity.exe` and `dist\UnheardCity--windows-amd64.zip` -(~78 MB zip, ~190 MB unpacked). The build also writes `THIRD_PARTY_LICENSES.txt` (license texts come from the -wheels and from `tools/licenses/`). +(~86 MB zip, ~205 MB unpacked, catalogs included). The build also writes `THIRD_PARTY_LICENSES.txt` (license +texts come from the wheels and from `tools/licenses/`). **Installer.** On Windows the folder (and zip) also contains `Install.cmd`, `install.ps1` and `uninstall.ps1` (sources in `tools/installer/`, version filled in by the build). It is a plain PowerShell installer, so no diff --git a/docs/reverse-engineering/findings.md b/docs/reverse-engineering/findings.md index 2109b4f..846a6b6 100644 --- a/docs/reverse-engineering/findings.md +++ b/docs/reverse-engineering/findings.md @@ -85,6 +85,10 @@ Conclusion: approach A works end-to-end in-game. Use >= 30 s probes for the intr used on purpose); a breakpoint on the setup parser entry: no extra events. Hypotheses: no in-memory sound plays without input; the decode path is the same as for WAD sounds (same WEM rules, 300/300 in-memory WEMs rebuild byte-identically), so playback is expected to work like E4/E10. +19. (2026-10-09) In-game text test (`ackit verify --ingame --texts`, ITA): 12 menu texts of FRONTEND/GFxUI + prefixed with markers and 12 subtitles patched in place (21 files). All 12 markers are found as UTF-16 in the + game's memory after ~45 s (the localization files are read at startup), no crash, revert byte-identical: + PASS. Subtitles are not loaded on the title screen (no dialogue object in Frontend*/Startup/BmGame). ## Old path -> new path `scripts/harness.py` -> `ackit verify --ingame` (ackit/verify/ingame.py); `scripts/validate_builder.py` -> diff --git a/pyproject.toml b/pyproject.toml index b6e36b7..55f7cd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ackit" -version = "2.0.0" +version = "3.0.0" description = "Batman: Arkham City (PC) audio toolkit and editor" readme = "README.md" requires-python = ">=3.10"