6.3 KiB
EngineCtx struct — typing the engine context — design
Date: 2026-07-09
Status: implemented 2026-07-09 (3826064, 0ecd776)
Home in the canonical map: a NEW canonical source (vm-map/engine-ctx.toml) + a generated
reference (docs/engine-ctx-reference.md); add a row to the CLAUDE.md canonical-documents map and the
single-source-of-truth table. RE narrative stays in docs/engine-re.md (which already documents the
offsets); this struct is the machine-readable + applied form of that knowledge.
Motivation
The recurring RE friction that survives the handler-labeling and import-map work is re-decoding the
same ctx (esi) offsets by hand — every handler RE means reading *(int*)(param_1 + 0x53d14),
+0x53d88, +0x9b24c, +0x408, +0x52bd4, +0x5f304, +0xa0ce4… and recalling what each means. We
have already documented ~30 of these in docs/engine-re.md. Defining them once as a Ghidra struct
and applying it makes every handler decompile with named fields (ctx->cur_gfx_idx), front-loading the
"what's this offset" lookup so it never recurs. This is the same shape of win as Task A (handler
labeling): a one-time pass that pays out on every future look at the decomp.
Scope note: this is strictly the engine context struct (esi/thiscall this). The VM global bank
G[…] is a separate address space (not ctx offsets) and is out of scope here (it has its own map,
vm-map/globals.toml).
Architecture
Single source of truth → generated machine view + doc → applied to the Ghidra image. Mirrors the
opcodes.toml/globals.toml pattern exactly.
1. vm-map/engine-ctx.toml (canonical source — hand-edited)
[meta]
struct_name = "EngineCtx"
size = 0xa1000 # >= max field end; sparse (gaps = undefined). Grows as needed.
[[field]]
offset = 0x53d14
name = "cur_ctx_index"
type = "uint"
note = "current gfx-object / script-context index (curCtx); indexes the 0x78-byte context records"
source = "native-RE"
confidence = "high"
# … one [[field]] per documented offset …
Seeded with the ~30 fields already documented in engine-re.md, e.g.: 0x408 retained gfx-object map
(std::map; geometry/draw get-or-create, 0x215 queries obj+4 source slot, 0x1f7 erases), 0x40c
SYS4INI record count, 0x410 archive-name table, 0x414 SYS4INI 80-byte record
base, 0x14d54 obj-pointer table, 0x14f45 script-frame index, 0x46d14 stride-0x14 query table,
0x51b78/0x51b7c anim clock elapsed/duration, 0x52bd4 surfaces[1000], 0x53d14 cur-ctx-index,
0x53d28/0x53d2c frame codebase/PC, 0x53d60 context-record base (stride 0x78), 0x53d64 gfx
object-record array, 0x53d88 per-object cmd-type table (stride 0x78), 0x55120/0x55124/0x5512c
anti-tamper checksum, 0x55248/0x552e8 per-frame return stack, 0x5f304 sleep-timer object, 0x6da88/ 0x6db28 coroutine yield-state, 0x6dbc8/0x6dbcc coroutine resume-state, 0x9b24c dispatch handler
table[0x400], 0xa0cc0/0xa0cc4/0xa0cc8 screen w/h/bpp, 0xa0ce4 run-state flags, 0xb558/0xb560 gfx
dirty flags.
Types kept simple: int/uint/void*; arrays where clearly arrays (dispatch[0x400] as
void*[0x400], surfaces[1000]); a field may be a plain scalar even if it's the head of a table (the
struct only needs the offset named — the array modelling is optional polish).
2. tools/engine_ctx_build.py (generator + linter)
--build→build/engine-ctx.json({ "0x53d14": {"name": "...", "type": "uint"}, ... }+ meta) and regeneratesdocs/engine-ctx-reference.md(offset / name / type / note table, grouped).--lint→ fail on: overlapping fields (offset+size collisions), a field whose end exceedsmeta.size, an unknown type, or a duplicate name. Run in--build.- Model + lint in the tool (or a small
engine_ctx_model.pyif it grows); unit-tested (test_engine_ctx.py).
3. Ghidra apply (run_script_inline, Java — the Task A pattern)
Confirm the active program is /v2/range_00400000.bin (base 0x400000, 4400+ fns — the two-program
gotcha). Then:
- Read
build/engine-ctx.json; create (or replace) a structEngineCtxofmeta.sizebytes with each field placed at its offset (struct.replaceAtOffset/insertAtOffsetper the Ghidra API), gaps left undefined. - Read
build/op-handler-map.json; for each dispatch handler, set itsthis/first-parameter type toEngineCtx *(via the thiscallthistype, or parameter 0 where the convention lacks athis). Skip functions that don't takectxas first arg (report them). - One transaction;
save_program.
Data flow
vm-map/engine-ctx.toml ──--build──▶ build/engine-ctx.json + docs/engine-ctx-reference.md
build/engine-ctx.json + build/op-handler-map.json ──run_script_inline──▶ EngineCtx struct +
retyped handler this on /v2 ──▶ handlers decompile ctx->field.
Validation
--lintpasses (no overlaps/OOB/dup).- Decompile
sleep_timer_arm— expectctx->sleep_timer(offset0x5f304) instead ofparam_1 + 0x5f304; decompilegfx_op_0x215_query_source_slot— expectctx->cur_ctx_index/ctx->cmd_type_table[...]style rendering at0x53d14/0x53d88. Record a before/after snippet. - Handler count retyped reported; functions skipped (no
ctxfirst-arg) listed for review. save_programsucceeds.
Scope & boundaries
- In: the ~30 documented
ctxfields; struct creation; retyping dispatch-handlerthisparams. - Out: the VM global bank (
globals.toml); worker functions (FUN_0047xxxx) — retype later as we touch them; exhaustive field discovery (grows incrementally via the toml, not a big-bang sweep); nested sub-structs (e.g. modelling the 0x78-byte context record as its own type — a later refinement). - Regenerable:
build/engine-ctx.json+docs/engine-ctx-reference.mdare generated — never hand-edit; editengine-ctx.tomland re-run--build.
Acceptance criteria
vm-map/engine-ctx.tomlholds the ~30 seed fields;--buildregenerates the JSON + doc;--lintclean.EngineCtxstruct exists in the/v2image; dispatch-handlerthisparams retyped (count reported).- The two validation decompiles render named
ctx->…fields;save_programOK. - CLAUDE.md canonical-map + single-source table gain the
engine-ctx.tomlrow;tools-reference.mdgainsengine_ctx_build.py; status memory records the milestone.