In-game text test, version 3.0.0, report
This commit is contained in:
@@ -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
|
||||
|
||||
6
NOTES.md
6
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).
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
87
REPORT.md
87
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_<LANG>.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\<LANG>\*.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 `<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
|
||||
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/<lang>/*.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/<lang>/*.txt`) and rebuild the catalog.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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-<ver>-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
|
||||
|
||||
@@ -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` ->
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user