Imported from halixm-dev/hok-ace-patch (
AGENTS.md). Install upstream withnpx skills add halixm-dev/hok-ace-patch. Copyright stays with the author.
HOK ACE Language Patcher
Windows GUI that installs the English or Indonesian translation into 王者万象棋 (com.tencent.tmgp.osgame) on an Android phone over USB, without root. The Android app in android/ runs the same pipeline on the phone through Shizuku.
Python 3.12 only. resources/runtime/ is 3.12 bytecode and will not load on any other version.
Commands
Run from the project root:
setup.bat :: .venv + requirements.txt (fallback: set PYTHON=C:\path\to\python3.12.exe first)
build.bat :: -> dist\HOK ACE Language Patcher.exe
run.bat :: run from source
Headless runtime check (needs a dumped game directory):
"dist\HOK ACE Language Patcher.exe" --selftest <game-dir> <out-dir>
No test suite. Verify changes with run.bat or --selftest. The texture codec has its own offline check, which needs neither a device nor a game dump:
.venv\Scripts\python.exe tools\research\check_graphics.py [--game <game-dir>]
The whole pipeline also runs offline against a dumped game directory. Add the cached APK to include prime-only content. The builder is optional and needed only when REBUILD_PACKAGES names a package:
.venv\Scripts\python.exe tools\build_device_patch.py --runtime resources\runtime --game <game-dir> ^
--catalog resources\catalog --work <work-dir> --output <out-dir> ^
--apk <path>\installed.apk
After changing a user-facing string in either patcher, run tools\research\check_ui_strings.py.
After moving or renaming a doc, run tools\research\check_references.py and tools\research\check_staging_contract.py.
Architecture
| Path | Role |
|---|---|
app/ |
GUI (hok_patcher.py), device workflow (patcher_core.py), APK rewrite + v2 signing (apk_tools.py) |
tools/ |
Pipeline: languages, terminology, staging (build_device_patch), rebuild (package_rebuild, qts_builder), append (qts_insert), textures (device_extensions, sprite_table, mobile_graphics), extra text (extra_text, pb_text, unity_text), relocation (qts_relocate) |
tools/research/ |
Probes and offline checks, run by hand |
resources/runtime/ |
Compiled wxq_patch translation runtime (from the PC patcher) |
resources/catalog/ |
English catalog (catalog.json.gz) plus the English catalog edits (.blocks / .assets) |
resources/catalog_id/ |
Indonesian catalog, plus only the catalog edits it re-draws (<HASH>.blocks + previews/<HASH>.png); every other payload is read from catalog/ |
resources/sprites/ |
This repo's own art for regioned atlases: <HASH>/labels/ and <HASH>/restore/ |
resources/ui/, resources/fonts/ |
The shared design's theme tokens, strings and fonts, read by both patchers (DESIGN.md) |
resources/qtsvfs/ |
Tencent's own package builder (QtsVFS.dll + QtskDB.dll + VC runtimes), driven by ctypes |
Flow: patcher_core drives the seven device steps and calls build_patch; the GUI talks only to patcher_core.
sys.path is set in patcher_core.py: tools/ is inserted for source runs; resources/runtime/ is appended so the project stdlib wins.
Staging contract
The staging summary contract (payload fields, mode values, skipped lists, pass order, APK/builder gating) is the build_patch docstring in tools/build_device_patch.py.
Read it before editing the pipeline, and change it in the same edit as the code; tools/research/check_staging_contract.py checks it stays there.
Code style
Module docstring, from __future__ import annotations, type hints, pathlib.Path. Constants UPPER_CASE at module level. Private helpers _name. User-facing failures raise PatchError.
Constraints
Install and delivery
- Install path: re-sign, install the access APK, write the override layer, reinstall the normal APK. The access APK never stays installed, nor is it cached as the normal APK. (ADR 0001)
- Update timing: write the translation only after the in-game update finishes. (ADR 0002)
- Normal APK: the game's own file with only
AndroidManifest.xmlrewritten (debug flag, launcher nameAPP_NAME); any further edit needs a device cycle. The rename must never fail a run. (standalone-patcher-app, device-cycle) - Pristine delta: after a verified install each patcher keeps the pristine bytes it overwrote (
tools/pristine_delta.py), and a later run rebuilds the pristine layer from it before building. A run that can neither rebuild nor read the layer as pristine refuses before any game file is written. The delta always points at pristine; the two patchers never share one. (language-switch spec) - Login: re-signing breaks direct WeChat/QQ login; users log in via Scan to login, as expected.
- Working data (~8 GB) goes to the user-chosen folder, never into this repo.
build/,dist/,.venv/are generated and disposable.
Coverage and gating
- Coverage is the staged text tables, the catalog edits, the two Career-atlas labels (
mobile_graphics.ATLAS_LABELS) one Lua label (extra_text.SCRIPT_LABELS) and one Lua format edit (extra_text.SCRIPT_EDITS). Never call it a complete translation; state the texture count as applied, never covered. (translation-progress) - Catalog only: the text pass stages the catalog's tables and nothing else (
_drop_uncatalogued_tables). (prime-only-append) - Allowlist: a config bin is translated only once a device cycle clears it onto
extra_text.SAFE_ASSETS(empty today).--cycle-assetenables oneCANDIDATE_ASSETShash per run;tools/extra_text.pyrecords each withdrawn hash with its failing screen. (ADR 0004, extra-text-cycles) - Uncatalogued config bins are never rewritten, not even re-pointed at identical length. (ADR 0005)
- Card dictionaries keep their chunk size and gain chunks only with keys in
device_extensions.NATIVE_CHUNK_KEYS; unknown layouts are refused and reported. No text table gains rows past its existing chunks. (prime-only-append) - Both data layers: check the override layer and the prime layer before calling an asset unreachable. (diagnose-untranslated)
- Aliases: an entry joins
mobile_graphics.DEVICE_REVISIONSonly on a measured match, never by guess. (texture-translation)
Pipeline
- Append, not rebuild:
REBUILD_PACKAGESstays empty (package21in particular); prime-only content reaches the override layer by append. (ADR 0003) - Append pass rewrites nothing it did not add, keeps the read-back through
QtsPackage, stays idempotent, and reports a full B-tree leaf (TreeFull) inappend_skippedinstead of failing the run. A new asset kind it may carry goes intopackage_rebuild.make_transform. (prime-only-append) - APK gating: without the APK a run still succeeds and leaves prime-only content Chinese; check the log for the
added from the APKlines. (prime-rebuild) - Reading chunks:
qts_relocate.read_rawfollows each chunk's own framing, not the entry's compression id. A rebuild omits entries with no bytes (entries_without_bytes) but aborts on bytes that will not decode. (prime-rebuild) - Regioned atlas: placed by the device's sprite table; a missing or moved region refuses the texture, with no fallback to the PC offsets. (ADR 0006, texture-translation)
- Skipped textures: a texture
english_rawcannot handle returnsNoneand counts as skipped, never raised.splice_labelstays idempotent. (texture-translation) - Counters: the GUI shows untranslated counts beside
count_translated's(text tables, textures)(see the staging contract). - Terminology table: one game keyword reads one way, the default name (
夺取is Steal, never the community Plunder). It applies in memory as the catalog loads. Add a term only with its variants measured on both catalogs. (terminology-table) - Tokens:
{Energy}/{LordExp}/{LordHp}are expanded in the staged replacements byresource_tokens.expand_tokens; the catalog keeps the tokens. (resource-token-expansion) - Update strings: strings a data update adds come from
tools/catalog_supplement.py, only where the catalog has no entry, and must passadaptive.validate_translation.
Freezing
- Runtime bytecode:
resources/runtime/is binary. A stdlib import it needs at freeze time goes inHIDDEN_IMPORTSinapp/build_exe.py. - Codec modules:
app/build_exe.pycollectsastc_encoder,oozandtexture2ddecoderexplicitly andpypinyinwith--collect-all; without them the exe builds but dies on the first texture. (texture-translation)
Boundaries
resources/key/holds the re-signing key and ships on purpose. Keep it stable across releases; never regenerate it.resources/qtsvfs/is Tencent's builder and ships on purpose. Treat the DLLs as immutable; never hand-write a package layout in their place. (prime-rebuild, "The keys the builder derives")resources/catalog/,resources/catalog_id/catalog.json.gzandresources/runtime/come from the PC patcher pipeline. Fix a record in this repo's code (asmobile_graphics.MISSING_METADATAdoes); the one sanctioned catalog write istools/research/apply_catalog_fixes.py, re-run after every upstream import and extended throughtools/research/catalog_additions.json(new strings),tools/research/catalog_rewrites.json(garbled ones) andtools/research/catalog_english_fixes.json(one English slip repeated across many strings, such as "Usage" as a verb).catalog/ships beside every other catalog, because a language falls back to its payloads;resources/catalog_id/'s own payloads come fromtools/research/make_language_payloads.py(--checkproves them current). (indonesian-language)resources/sprites/:restore/is generated bytools/research/atlas_sprites.pyfrom a pristine layer only.labels/is hand-finished art and the source of truth; never regenerate over it, and runlabels --checkandtools/research/career_atlas/validate_assets.py --delivered <dir>before shipping a change. (texture-translation)tools/research/is a research record, not shipping code: nothing imports or bundles it.
Docs
Every kind of doc has one home. Write prose in the vocabulary of GLOSSARY.md.
| Kind | Home | Read it when |
|---|---|---|
| Rules for every turn | AGENTS.md |
always loaded |
| Vocabulary | GLOSSARY.md |
naming a concept |
| Product | PRODUCT.md: users, purpose, voice |
changing either patcher's interface or copy |
| Design | DESIGN.md, for both patchers |
changing either patcher's interface |
| Decisions | docs/adr/ |
changing an area an ADR names |
| Feature records | docs/features/ |
changing a pass; update it in the same change |
| Device cycle | docs/runbooks/device-cycle.md |
testing a change on a phone |
| Untranslated string | docs/runbooks/diagnose-untranslated.md |
a string stays Chinese |
| Crash | docs/runbooks/read-a-crash.md |
the game crashes |
| Evidence | docs/lab-notebook.md, the single source for numbers and verdicts |
before re-running an experiment |
| Coverage | docs/translation-progress.md |
a change moves a count; update it in the same change |
| Staging contract | the build_patch docstring |
editing the pipeline |
| Work in flight | .scratch/<feature>/, committed |
working a ticket |
| Skill configuration | docs/agents/ |
read by the skills |
Where a record and this file disagree, this file wins; a run's own summary outranks the coverage ledger.
Git
Conventional commits: feat:, fix:, chore:, docs:. Update README.md in the same change when commands or layout change.
Agent skills
Issue tracker
Issues and specs live as local markdown files under .scratch/<feature>/. See docs/agents/issue-tracker.md.
Triage labels
The five default triage roles, each label the same as its role name, written as a Status: line in each issue file. See docs/agents/triage-labels.md.
Domain docs
One GLOSSARY.md and docs/adr/ at the repo root. See docs/agents/domain.md.
