In-game text test, version 3.0.0, report

This commit is contained in:
2026-10-09 22:50:15 +02:00
parent a10593ec24
commit 98ec2fe9c4
10 changed files with 123 additions and 40 deletions

View File

@@ -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). 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`). - 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), - 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). checks byte-identity itself).
- Rebuild catalogs only from original game files: `python tools/build_catalog.py` (~5 min, scans all packages). - 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 - Shell: git at `C:\Program Files\Git\cmd` (prepend to PATH). Write Python with the Write/Edit tools; avoid

View File

@@ -36,7 +36,11 @@
counter, apply/revert/revert all, find & replace on the filtered rows, go to sound); the sound editor 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 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. 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; - OPEN: channel order for >2 channels unverified; speech and in-memory sounds *heard* in-game (need gameplay;
posted keys do not drive the menu). posted keys do not drive the menu).

View File

@@ -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). 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`.

View File

@@ -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) ## What works (verified)
- **Repository**: clean layout (`ackit/`, `app/`, `tests/`, `data/catalog/`, `docs/`, `tools/`); old state in git - **Every sound of the game is cataloged and editable**: the speech WADs (5 languages x 10,816), the SFX WAD (651)
tag `pre-cleanup`. One CLI (`ackit`) and one app (`UnheardCity`). and, new, the **15,404 in-memory sounds** stored inside soundbanks (archive `BANK`: footsteps, foley, combat,
- **Toolkit**: portable encoder (libvorbis via libsndfile; no Wwise, ffmpeg or vgmstream), WAD/pack injector, weapons, UI, efforts) in loose `.bnk` files and in 1,499 `.upk` packages. ackit reads and patches the
originals store with byte-exact revert, conflict detection, mod packages, Steam/Linux/Proton game discovery. LZO-compressed UE3 packages itself (`ackit/upk.py`, `lzo.py`; no external tools). A replacement goes into every
- **Catalog**: CSVs for ITA/INT/FRA/DEU/ESN/SFX (11,467 sounds per speech language set), categories, verified copy (93,910 copies); 396 speech lines with prefetch heads in banks get their heads rewritten too.
speakers from the user's lists (3,366 lines in every language), inferred characters for the rest, original Names: 6,456 BANK sounds are named after their Wwise event (`Play_...`), the rest after their bank.
sizes and checksums. Schema in `data/catalog/README.md`. - **Catalog check**: `ackit verify --catalog` compares the catalog with a fresh scan of the game (missing, extra,
- **Editor**: browse/search/filter, listen (A/B, loop, waveforms), replace from file or recording, trim/gain/ duplicate, size/checksum, category, character, language, bank copies, subtitle copies/texts): **catalog OK**
normalize/loudness, apply/revert/revert all, changed-underneath warnings, notes/tags/favorites, mod on the full game. It found and fixed 2 duplicate names per language (variation shown as `NAME#i`).
export/import, settings. - **Subtitles**: found in `RDialogueEvent` objects inside the packages (9,016 speech lines x 11 languages,
- **Tests**: 75 automated tests (synthetic game, real-game read-only tests, offscreen GUI tests) pass. 168,122 stored copies), parsed, cataloged (`SUB_<LANG>.csv`) and linked to their speech sound through the
`ackit verify` (offline self-check on the installed game) PASS. soundbanks (92 % of the strings; the rest are unused/placeholder lines). Edited in place in every copy.
- **Real game**: `ackit verify --ingame` PASS (ncc 1.0); `tools/e2e_editor.py` — replacement applied through the - **Menu text**: the localization files (`BmGame\Localization\<LANG>\*.int`, ~7,000 strings per language),
editor is heard in the game (ncc 1.0, no crash), revert leaves WAD and pack index byte-identical; a speech line edited value by value; everything else in the files stays byte-identical.
applied and reverted byte-identically. Game files are original at the end. - **Editor**: tabs *Sounds* / *Subtitles* / *Menu text*. The sound editor shows and edits the selected speech
- **Package (Windows x86-64)**: `dist/UnheardCity-2.0.0-windows-amd64.zip` (78 MB) with line's subtitle side by side; the subtitle list links back to the sound. Search, filters (character,
`THIRD_PARTY_LICENSES.txt`; runs its smoke test and starts the GUI with `PATH` reduced to `C:\Windows`. category, file, modified), room counter, apply/revert/revert all, find & replace on the filtered rows, mod
Contains a per-user installer (`Install.cmd`): install, upgrade and uninstall (Settings > Apps) verified. 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 ## Unresolved / limitations
- **Coverage**: only sounds in the WAD archives are listed and editable. About 15,800 short effects (footsteps, - **Heard in game, not proven automatically**: in-memory sounds and subtitles. The game loads the patched
foley, weapons, UI, grunts) are stored inside soundbanks (loose `.bnk` and banks embedded in `.upk` packages (probe data and texts found in its memory, no crash), but nothing of this plays/shows on the title
packages) and are not covered yet (`docs/formats/banks.md`). screen without input, and keys posted to the game window do not reach the menu (findings 18). Speech lines
- Speech lines being *heard* in the game needs gameplay; verified indirectly (same decoder/WAD path as the music were already in this situation; the decoder path is the same one proven for music.
test, byte-level apply/revert, boot test with modified speech WAD). - **Size limits**: in-memory sounds and speech must fit their original size; subtitles must fit the original
- Speech/effects replacements must fit the original's size (index lives in compressed `.upk` packages). text's length ("room"); cp1252 subtitles accept cp1252 characters only. Lifting these limits means moving
- Channel order for 4-6 channel sounds is unverified (tests used identical channels). objects inside the packages (new export offsets/sizes, added chunks): designed but not done, because it could
- Inputs: WAV/FLAC/OGG/OPUS/MP3/AIFF built in; M4A/AAC/WMA only with an ffmpeg installed on the system. not be verified automatically in the game (decisions 4c).
- Character labels outside the user's lists are inferred from names (marked as such), not verified speakers. - **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 `<Bank>_<n>`.
- 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 ## Next steps
1. Support the in-memory sounds in soundbanks: the loose footstep banks first, then the banks embedded in 1. Gameplay check (a person): a replaced footstep/foley sound and an edited subtitle in a mission.
`.upk` packages (`docs/formats/banks.md`). 2. Optional: lift the subtitle length limit by relocating dialogue objects (needs that gameplay check).
2. Extend the curated speaker lists (`data/catalog/source/<lang>/*.txt`) and rebuild with `tools/build_catalog.py`. 3. Extend the curated speaker lists (`data/catalog/source/<lang>/*.txt`) and rebuild the catalog.
3. Optional: rewrite the BKPK indexes inside `.upk` packages (LZO) to lift the size limit for speech.

View File

@@ -8,4 +8,4 @@ Library layers (low -> high):
inject (WAD writing), originals (store), workspace (apply/revert), modpack (share) inject (WAD writing), originals (store), workspace (apply/revert), modpack (share)
tools cli (`ackit` command), verify/ (offline self-check, in-game test on Windows) tools cli (`ackit` command), verify/ (offline self-check, in-game test on Windows)
""" """
__version__ = "2.0.0" __version__ = "3.0.0"

View File

@@ -310,6 +310,8 @@ def cmd_verify(a):
from .verify import ingame from .verify import ingame
if a.banks: if a.banks:
rep = ingame.run_banks(ws, a.sound or None) rep = ingame.run_banks(ws, a.sound or None)
elif a.texts:
rep = ingame.run_texts(ws)
elif a.baseline: elif a.baseline:
rep = ingame.run(ws, a.target, mode="baseline", seconds=a.seconds) rep = ingame.run(ws, a.target, mode="baseline", seconds=a.seconds)
elif a.installed: elif a.installed:
@@ -390,6 +392,7 @@ def build_parser():
s.add_argument("--banks", action="store_true", s.add_argument("--banks", action="store_true",
help="with --ingame: test in-memory sounds (default: the front-end banks, or --sound NAME ...)") 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("--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 # advanced
s = add("encode", cmd_encode, "[advanced] audio -> .wem file", (enc,)) s = add("encode", cmd_encode, "[advanced] audio -> .wem file", (enc,))
s.add_argument("audio"); s.add_argument("out"); s.add_argument("--like", metavar="NAME") s.add_argument("audio"); s.add_argument("out"); s.add_argument("--like", metavar="NAME")

View File

@@ -121,6 +121,57 @@ def run(ws, key=DEFAULT_TARGET, wem=None, mode="modded", seconds=40.0, threshold
return report 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) ------------------------------------------ # --- 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) PROBE_RATE, PROBE_Q = 28000, 4 # a codebook setup the game's own WEMs do not use (E10)

View File

@@ -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 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. images the build still works, with a plain icon and no wallpaper.
Output: `dist\UnheardCity\UnheardCity.exe` and `dist\UnheardCity-<ver>-windows-amd64.zip` Output: `dist\UnheardCity\UnheardCity.exe` and `dist\UnheardCity-<ver>-windows-amd64.zip`
(~78 MB zip, ~190 MB unpacked). The build also writes `THIRD_PARTY_LICENSES.txt` (license texts come from the (~86 MB zip, ~205 MB unpacked, catalogs included). The build also writes `THIRD_PARTY_LICENSES.txt` (license
wheels and from `tools/licenses/`). 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` **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 (sources in `tools/installer/`, version filled in by the build). It is a plain PowerShell installer, so no

View File

@@ -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 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 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. 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 ## Old path -> new path
`scripts/harness.py` -> `ackit verify --ingame` (ackit/verify/ingame.py); `scripts/validate_builder.py` -> `scripts/harness.py` -> `ackit verify --ingame` (ackit/verify/ingame.py); `scripts/validate_builder.py` ->

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "ackit" name = "ackit"
version = "2.0.0" version = "3.0.0"
description = "Batman: Arkham City (PC) audio toolkit and editor" description = "Batman: Arkham City (PC) audio toolkit and editor"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"