docs: update living refs — engine-dump tooling, gfx drift = state-divergence

tools-reference: dump_engine/probe_handlers/capture_gfx_objects + SYS4AB note.
phase-a-slice-plan: post-opening drift RESOLVED as a state-divergence artifact
(fix = Phase B state flow, not a native-op subsystem). PROJECT-STRUCTURE:
build/{textures,engine-dump}, engine/, tools/frida/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-07 00:10:45 -04:00
parent 896bea977b
commit 31a5423ad2
3 changed files with 34 additions and 12 deletions

View File

@@ -75,11 +75,16 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ │ └── strings.jsonl every string, tagged by source opcode
│ ├── data/ parsed data tables (*INIT → JSON)
│ ├── scripts-json/ machine-readable full dumps (on demand via --json)
│ ├── textures/ AGF→BMP stills (convert_agf.py) — feeds the Godot render
│ ├── engine-dump/ UNPACKED engine dump (frida/dump_engine.py): range_<base>.bin + manifest.json
│ ├── asset-index.json, asset-sections.json asset resolver data (parse_sys4ini / resolve_asset)
│ ├── global-var-map.{json,md} partial global-variable name map
│ ├── opcodes.json GENERATED from opcodes.toml (machine view for the C# VM)
│ └── manifest.json, opcode-coverage.md (opcode-coverage.md GENERATED from opcodes.toml)
── godot/ DELIVERABLE — the Godot/C# engine project (built in Phase A2+)
── engine/ DELIVERABLE — the .NET VM core (AgeEngine.sln: Age.Engine / Age.Cli / tests)
├── tools/frida/ runtime-capture + engine-dump scripts (see tools/frida/README.md)
└── godot/ DELIVERABLE — the Godot/C# ADV front-end (references Age.Engine)
```
## Conventions

View File

@@ -268,16 +268,28 @@ measured `0×0`, and the anchor-preserve math (`base' = center (w_new/2, h_n
corruption. Fix: seed `_slotDims[0] = (800,600)` (and record `create-texture(w,h)` dims) so the first CG's
anchor stays an identity. This is the faithful stand-in for the skipped boot-time primary-surface creation.
**DEFERRED (next chunk) — the sprite/background anchor-record subsystem.** Everything blits through slot 0
as an immediate-mode canvas; the anchor-preserve base globals **accumulate drift** across textures of
*different* sizes (same-size 800×600 CGs stay put; the first `BG*` 800×500 / `AE*` 800×800 / sprite starts
a drift that accumulates — `BG030A→(300,500)`, next→`(450,100)`, `(800,350)`, marching bottom-right).
The real engine doesn't drift because it stores each element's geometry in a **per-object record** via
`0x217/0x218/0x21a` (currently no-op) and restores it (the `0x12683` if-branch reading the `0x3239` record
table). Implementing that store/restore (+ the record layout, likely Frida-confirmed) is the fix for
backgrounds **and** sprites together. Fades/alpha (`AE*`, `0x202/0x203`) and green chromakey remain
deferred as before (the compositor is built to accept alpha later). Also out: true multi-surface (dest
handle is collapsed onto the screen). The full-screen opening path is unaffected by any of these.
**Post-opening bg/sprite drift — RESOLVED as a STATE-DIVERGENCE artifact, NOT a missing native op
(2026-07-06/07).** Symptom: everything blits through slot 0 as an immediate-mode canvas; the anchor-preserve
base globals **accumulate drift** across differently-sized textures (`BG030A→(300,500)`, next→`(450,100)`,
`(800,350)`… marching bottom-right). We reverse-engineered the whole chain (systematic-debugging):
1. Root cause traced to **`0x215` = native graphics-object query** (opcodes.toml `query-gfx-object?`), which we
stub → `label_12649` takes the wrong branch → all draws collapse onto slot 0 → anchor-preserve reads foreign
textures → drift.
2. **Engine now statically analyzable (major, general unlock):** `SYS4AB.BIN` = `XOR-0xFF(AGE.EXE)` (dead end),
but `AGE.EXE` unpacks **in-place at 0x400000** in the live process → `tools/frida/dump_engine.py`
`build/engine-dump/` (validated via AGF-decoder landmark; interpreter confirmed to run from the module, so
handlers are hookable). Handler ABI + object-record layout (`[esi+0x53d64]`, 120B/rec, cmd-type at rec+0x24)
decoded. See `docs/vm-mapping-plan.md` appendix + `tools/frida/README.md`.
3. **Live capture verdict (the resolution):** `tools/frida/capture_gfx_objects.py` polled the object-record
array through the **real** opening — it held only **3 persistent UI objects, ZERO CG objects**. So the real
game does **not** draw the opening CGs via the `0x2120x21a` positioned-object path our headless VM uses;
with proper state it takes a different (direct) branch that we already render correctly. **⇒ the drift is
downstream of our unseeded headless VM taking `label_12649`'s else-branch (compute-from-drifting-base) where
the real game hits the if-branch (stored/record geometry). The fix is the Phase B state/choices-flow work,
not a separate native-op subsystem.** Seeding real per-scene/object state makes `label_12649` branch right.
Fades/alpha (`AE*`, `0x202/0x203`) + green chromakey + true multi-surface remain deferred; the compositor is
built to accept alpha later. The full-screen opening path is correct and unaffected. **Native gfx-op modeling
is only needed for scenes that genuinely use runtime-positioned sprites — revisit later with the dump in hand.**
---

View File

@@ -78,10 +78,15 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited *
| `tools/frida/capture_resid_args.py` | Phase-2 probe: dumps the decoder's args / context / caller frame (established the loader carries only offsets, not names). | `py -3.11 -u -X utf8 tools/frida/capture_resid_args.py [pid]` · `--analyze` | running game → `build/frida-resid-args.jsonl` |
| `tools/frida/find_globals_base.py` | Runtime-global RE (SHELVED — see `docs/global-memory-re.md`): flat-int32 signature scan for the VM global array. Finds nothing → layout isn't flat. | `--build-sig` · `py -3.11 -u -X utf8 tools/frida/find_globals_base.py [pid]` | `*INIT``build/globals-signature.json`; scans running game |
| `tools/frida/find_global_by_sequence.py` | Runtime-global RE (SHELVED): differential resId value-scan + stability filter. Finds stack proxies; proved `G[0x62424]` is a transient arg-register. | `py -3.11 -u -X utf8 tools/frida/find_global_by_sequence.py [pid]` | running game + index → stdout |
| `tools/frida/dump_engine.py` | ★ **Dump the UNPACKED engine code** from the live process for offline static RE (native handlers). `AGE.EXE` unpacks in-place at `0x400000`; Kelebek VAs map `VA0x400000` = file-off. Validated via the AGF-decoder landmark `+0x74f1f`. | `py -3.11 -u -X utf8 tools/frida/dump_engine.py [pid]` | running game → `build/engine-dump/{manifest.json,range_<base>.bin}` |
| `tools/frida/probe_handlers.py` | Probe which region the interpreter executes from (module vs heap). Confirmed: **operand-fetch `+0x1b940` fires ~8500/s ⇒ interpreter runs from the module `0x400000`** (handlers hookable by dump address). | `py -3.11 -u -X utf8 tools/frida/probe_handlers.py [pid]` | running game → stdout (per-hook fire counts) |
| `tools/frida/capture_gfx_objects.py` | Capture the native gfx object-manager state: grab engine ctx (`esi` via operand-fetch `ecx`), poll the object-record array `[esi+0x53d64]` (20×120B; `field[0]=0xffffffff`=free, cmd-type at rec+0x24). **Finding: the real opening uses ZERO CG records ⇒ the bg/sprite drift is a state-divergence artifact, not a missing op.** | `py -3.11 -u -X utf8 tools/frida/capture_gfx_objects.py [pid] [secs]` | running game → `build/gfx-objects.jsonl` |
*(Static disassembly of `build/engine-dump/range_00400000.bin` uses **capstone** — `py -3.11 -m pip install capstone`; VA `X` → file offset `X0x400000`.)*
## Historical / one-off
| Tool | Purpose |
|---|---|
| `probe_*.py` (`probe_header`, `probe_leads`, `probe_refs`, `probe_tables`, `probe_tags`, `probe_types`, `probe_xref`) | Container/opcode format-RE probes used to reverse the format originally. Kept for reproducibility; not part of the normal workflow. |
| `pack_check.py` | Checks whether `AGE.EXE` is packed (it is). No longer a blocker — we run our own VM. |
| `pack_check.py` | Checks whether `AGE.EXE` is packed (it is: entropy-8 code sections, zeroed IAT). `SYS4AB.BIN` is NOT a separate image — it's `XOR-0xFF(AGE.EXE)` byte-for-byte (0x2c header + XOR payload). The unpacked engine exists only in memory → dump it with `frida/dump_engine.py`. |