Files
OpenMaidEngine/docs/engine-re.md
gamer147 0ae4f607c0 docs: native-engine RE — Ghidra+MCP loop + opcode dispatch table + u00428010 correction
Ghidra+MCP workflow validated. Found the opcode->handler dispatch table
(*(ctx+0x9b8f4+op*4), registered by FUN_00413860) — the general fix for Kelebek VA
drift. Corrected: u00428010 (op 0x1a2) is a save/resource op, NOT decision->scene;
the SCJUMP consumer persists the visited-decision flag. New doc docs/engine-re.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 11:29:02 -04:00

5.9 KiB
Raw Blame History

Native-engine reverse engineering (Ghidra + MCP)

Static RE of the unpacked AGE.EXE engine image, driving Ghidra 12.1.2 via the bethington/ghidra-mcp bridge. This is the home for decompiled native-op findings — the class of logic the scripts call but that lives compiled in the engine (decision→scene, call-script dispatch, op 0x60, the gfx command-buffer). Opcode semantics recovered here also flow into vm-map/opcodes.toml.

Related: docs/scjump-progression.md (the SCJUMP decoder that hit this wall), name-resolution.md §1 (call-script), vm-mapping-plan.md appendix (why the exe is packed + the runtime-dump route).


Runbook — the Ghidra + MCP loop

One-time setup (done 2026-07-07):

  • MCP server: bethington/ghidra-mcp, cloned to S:\Game Hacking\ghidra-mcp. We used the prebuilt extension GhidraMCP-5.14.2.zip (installed in Ghidra via File > Install Extensions) — this skips the Maven/Java-21 build. The Python bridge runs from a venv (.venv, Python 3.11, pip install .); no uv needed. Registered in Claude Code via .mcp.json at the workspace root: {"mcpServers":{"ghidra":{"command":"S:\\Game Hacking\\ghidra-mcp\\.venv\\Scripts\\bridge-mcp-ghidra.exe","args":["--transport","stdio"]}}}.
  • In Ghidra: enable the GhidraMCP plugin (File > Configure) and Tools > GhidraMCP > Start MCP Server (serves http://127.0.0.1:8089/). The bridge talks to that; Claude reaches the bridge over stdio.

Loading the engine image (IMPORTANT — the language gotcha):

  • Import age-reimpl/build/engine-dump/range_00400000.bin (the module dump: 2,490,368 bytes, the full 0x400000 module image; VA→file offset = VA 0x400000).
  • Format = Raw Binary, Language = x86:LE:32:default, Image Base = 0x400000. Ghidra's language picker offers x86:LE:32:System Management Mode as the "closest" match — do NOT use it. SMM is a 16-bit segmented (segment:offset) variant for BIOS/SMRAM; it mis-decodes flat 32-bit code (it loaded with addresses like 0000:0000/0025:ffff and produced 0 functions). The plain default variant is correct and yielded 2,721 functions.
  • We drove the (re)import over MCP: import_file(language="x86:LE:32:default", compiler_spec="windows", auto_analyze=false)set_image_base(0x400000) before analysis (so absolute-address refs resolve) → run_analysis.
  • Load sanity check (AGF-decoder landmark): at VA 0x474f23, CMP word ptr [ESI + 0x4], 0x4d42 (the BM/BMP-magic check) confirms the image is correctly based + decoded.
  • Escalation (unused so far): tools/pe-sieve32.exe /pid <PID> /imp 3 /dmode 3 /dir <out> (run from PowerShell, not Git Bash — it mangles /flags) rebuilds the IAT into a clean PE. Only needed if raw-dump analysis is inadequate; it was fine for reading logic, so we stayed on the raw dump.

Master key — the opcode→handler dispatch table (2026-07-07)

The interpreter dispatches each op via a per-context handler table:

handler(op) = *(ctx + 0x9b8f4 + op*4)ctx = the engine context (esi in handlers, thiscall).

The table is populated by the registration routine FUN_00413860 as a long run of MOV dword ptr [ESI + 0x9b8f4 + op*4], <handler_va>. Sample (from ~0x414756):

slot offset handler VA
0x9b8f4 0x42bd90
0x9b8f8 0x427ed0
0x9b8fc 0x427fb0 (the save handler below)
0x9b900 0x416b70
0x9b904 0x428100
0x9b908 0x428240

Why this matters: the Kelebek u00XXXXXX opcode names encode handler VAs from Kelebek's build, which drift in our build (confirmed below). This table resolves the real handler for any opcode in our image — the general fix for VA drift across the whole project. (Anchoring the slot↔opcode index is the immediate next step: read the dispatch site's exact base, then slot = base + op*4.)

Other confirmed engine-context offsets (ctx/esi): +0x53d14 = current gfx-object index; +0x53d88 = per-object cmd-type table (stride 0x78 = 120 bytes); operand-fetch helper = call 0x41b940 (thiscall, ecx=ctx, arg = operand index → returns the operand value).


Findings

op 0x1a2 (u00428010) is a SAVE/resource-file op — NOT decision→scene (2026-07-07)

The SCJUMP slice assumed u00428010 resolved a decision value to a scene. That premise is wrong:

  • Kelebek's u00428010 = opcode 0x1a2, argc 1. In our build, VA 0x428010 is inside a different function (0x427fb0) — Kelebek-VA drift.
  • 0x427fb0 is a genuine interpreter handler (uses ctx+0x53d14, the ctx+0x53d88 cmd-type table, and the 0x41b940 operand-fetch). It builds file paths from the format string at 0x571e70 = %s\SAVE%2.2d.DAT (and a second at 0x571e84) via an sprintf-style 0x407770. It is multi-operand (fetches operands 2 and 3) — inconsistent with the argc-1 op 0x1a2, reconfirming the drift.
  • Conclusion: op 0x1a2/u00428010 is a save/resource-file op (matches its earlier tentative "resource" tag). So the SCJUMP consumer pattern lookup(0x5f0ed, decision); mov(ptr,1); u00428010(ptr) is persisting the "visited-decision" flag into the save data, not loading a scene. The real decision→scene resolution is a different mechanism — most likely the same native boundary as call-script/script-load (next investigation).

(To pin op 0x1a2's exact handler in our build: index the dispatch table above by 0x1a2.)


Native walls backlog (targets for this loop)

  • decision→scene — how 0x62ccf/the decision actually selects the next SCxxxx (re-aimed away from u00428010; likely call-script-adjacent).
  • call-script dispatchcall-script <id> → engine entry point (name-resolution.md §1).
  • op 0x60 (u0041A270) — the rand-like value gating 1732/1755 SCJUMP decisions.
  • gfx command-buffer — the 0x2120x21a positioned-object subsystem (scjump-unrelated; the rendering drift).