engine-re.md: the write-log method, plaintext-value insight (store is obfuscated),
vm_operand_write ABI, the packer poll-until-unpacked fix, and the validation
(34k-global capture seeds the VM to match the engine's whole opening). Plus the
resync-tolerant align() / operand-hook blind-spot notes. tools-reference: the new
tool + trace --state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
capture_global_writes.py hooks vm_operand_write@0x425fb0 (plaintext values,
before the obfuscated store — sidesteps the shelved flat-scan problem) and folds
the engine's global-int writes into a GameSession snapshot. --spawn captures from
boot (packer-aware: polls until the code unpacks, then attaches — AGE.EXE unpacks
in-place so a spawn-time hook hits packed bytes; also kills the spawned pid if
setup fails so no suspended orphan). Age.Cli 'trace ... --state <snap>' runs a
scene from the captured state.
Validated: a real boot->new-game->SC0000 capture (34008 globals incl. G[0x6c1]=1)
seeds the VM to match the engine's ENTIRE opening (542 ops, no non-realignable
fork) with zero manual seeding. Residual: a 2-op color detour (0x202/0x203)
unfixed by state = a real branch diff to chase. Engine 81/81, diff 5/5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Strict lockstep over-reported: the operand hook misses any op whose operands
aren't fetched via vm_operand_fetch (comment 0x1a7, set-string 0x192, the
0x1c7/0x1cc/0x131/... string-op cluster) — the engine executes them (they sit
after non-branching ops) but they're absent from its trace. New align() resyncs
over such one-sided insertions and reports only NON-realignable forks; it
separates VM-only blind spots (artifacts) from engine-only detours (real,
reconverging branch/state gaps, surfaced honestly). +2 tests (7/7).
Result: with G[0x6c1] seeded the SC0000 opening has NO non-realignable fork
across all 539 VM ops (was: false 'diverge at 0x8d'); residual = a 2-op
engine-only color detour (0x202/0x203 @ 0x122d0). Cold's first real fork is a
later G[0x6c1] gate. Validates the pre-scene-state theory end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
engine-re.md: the offset-path diff method, the working capture (operand hook
0x41b940; tick 0x410fb0 sees ecx!=ctx), the two capture caveats (hook-before-load
gating + operand-mode argc>=1 filter), SC0000 codebase id, and the first
divergence found (op 0xa0 jcc @ 0x8d on uninitialized G[0x6c1], the ADV-chrome
enable — a two-boot-gap state hole, not the predicted coroutine yield).
tools-reference.md: trace_engine_ops.py, diff_optrace.py, --trace-json.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Operand-hook captures skip zero-operand ops (stmt markers, script-entry 0x259),
so the VM offset trace is filtered to argc>=1 instructions before diffing — same
subsequence both sides. +1 unit test. Default on; --full for a tick-mode capture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reads cur_ctx_index/frame_pc/frame_codebase from the engine ctx per executed op
and emits (codebase, offset=(pc-codebase)/4) to build/engine-optrace.jsonl.
Recon result: the tick hook (0x410fb0) does NOT expose ctx via ecx (0 entries);
the operand hook (0x41b940) is the working capture (100% of offsets land on valid
SC0000 instruction starts). Writes a tracer-live.flag so the capture can be gated
on hooks-installed before the scene loads (else the entry burst is missed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure first_divergence + pick_scene_codebase (longest-common-prefix codebase
identification), unit-tested (test_diff_optrace.py, 4/4). CLI loads the engine
jsonl + VM json, isolates the scene's codebase, and reports the first divergence
with the mis-modeled instruction and +/-3 ops of context on each side (opcode
+ rendered line via sys4load).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JsonOffsetTraceSink records every executed instruction offset (bytecode word
index) of one target script, in order, filtered to the scene's own frame
(call-script subroutines excluded) to match the Frida engine tracer's
per-codebase filter. Wired as 'trace <SCENE.BIN> [--boot] --trace-json <out>'.
Observe-only; sweep path and trace parity untouched. +2 xUnit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Control-flow offset-path diff: Frida engine op-tracer (recon-gated
tick/0x41b940 hook) + VM ITraceSink offsets + diff_optrace.py first-
divergence report. Deterministic opening (SC0000 --boot). Ready to
execute in a fresh context; prereq = game running at the opening.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Function ID recon = WEAK (0 matches; bundled FidDbs don't cover the VC9
static runtime) and our library workers are already named. Pivoted to
naming the hottest documented-but-unnamed helpers by call-count: named 5
(~2400 call sites) in Ghidra — vm_operand_fetch@0x41b940 (1021 refs),
vm_operand_write@0x425fb0, vm_operand_lvalue@0x415f30,
__security_check_cookie@0x54f981 (692), operator_new@0x5502be (533).
Validated: gfx_op_0x215 reads near source-level. Spec/plan + engine-re.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Created the EngineCtx Ghidra struct (35 fields, 0xa1000) and retyped all
419 dispatch handlers' this -> EngineCtx* (CUSTOM_STORAGE, ctx in ECX).
Handlers now decompile ctx->cur_ctx_index / ctx->run_state_flags etc.
Validated: sleep_op_0xc8, gfx_op_0x215. tools-reference + engine-re.md
wired; CLAUDE.md canonical-map updated (root, outside repo).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Canonical source vm-map/engine-ctx.toml (35 documented ctx fields) +
engine_ctx_build.py (--build/--lint, unit-tested) -> build/engine-ctx.json
+ docs/engine-ctx-reference.md. Applied to Ghidra in the next task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
248 module-resident core imports at the RVA 0x16f000 rebuilt IAT
(kernel32/user32/gdi32/winmm/advapi32/ole/oleaut/version/ntdll);
29 singletons set aside. Anchors confirmed: ReadFile/CreateFileA/
SetFilePointer + timeGetTime@0x16f3d4 (=DAT_0056f3d4). d3d9/shell32
etc. are heap-resolved (out of dump) — expected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GATE-PASS: 23342 exports mapped, 277 in-range pointer matches clustering
into a module-resident import table at RVA 0x16f000 (VA 0x56f000);
23 singletons. ~254 real imports vs pe-sieve's 17-in-noise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the abandoned pe-sieve Task B: attach + scan the live process to
map resolved import pointers (RVA->dll!Func) and label /v2. Recon-first
hard gate; clean labels; read-only Frida.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pe-sieve /imp on the running game gave ~17 genuine imports (packer
bootstrap + one-per-DLL seed) and 300+ spurious stray-DWORD guesses:
the exe ships a zeroed IAT resolved via GetProcAddress, so there is no
conventional IAT to rebuild. Do not graft the output. Task B re-scoped
to a Frida live import-map (runtime_addr->dll!Func, RVA->name, label the
/v2 image); design captured in engine-re.md runbook.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract op->real-handler map (handler(op)=ctx[0x26c93+op]) from the
registration routine's override stores; ghidra_handler_map.py +
build/op-handler-map.json (420 overrides). Cross-check vs opcodes.toml
found 0 real drift. One-shot Ghidra pass then labeled the /v2 image:
281 raw FUN_/LAB_ handlers -> op_0xNN_handler, 107 bare VAs -> functions,
31 hand-named preserved, opcode plate comment on every handler.
Includes the Task A spec + plan and the two-program (/v2 vs SMM) gotcha.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Head-start for the next slice: the fix for the slot-0 collapse is running label_125bd
via the scene-coroutine framework. 0x7b=yield-save, 0x7c=resume, 0x140=LABEL (target
TBD), G[0xaba5c] gate. Plus the revealed magic-circle-persists-across-transition issue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Traced end-to-end (AGE_DIAG_SETTEX): set-texture slot=G[0x62452] <- query-gfx-object?
(-1 for unregistered CG handles) -> fallback lookup-array-2d(rec[s3]=G[0x3239])=0
because the slot table is never filled: label_125bd (SC0000 0x50f, slots 4..13) is
gated behind the scene-coroutine framework (0x140 coroutine-yield, stubbed). Adds
env-gated VM set-texture/query slot diagnostics + GfxState.IsRegistered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compositor applies TintStrength as a texel->tint LERP (0=keep texel) with object
opacity independent. New --gfx-log <file>: per-frame per-object draw/skip CHANGE
log + set-texture/create-texture slot trace — the tool that root-caused the grey
background (everything collapsing into slot 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause (evidence: gfx-log) of the opening-CG grey background: a CG drawn with
(alpha=0, color=white) means 'no tint' = fully opaque, but slice-A conflated the
color alpha with object opacity -> the CG rendered transparent. RenderObject now
carries TintStrength separately from Alpha (opacity); resolution keeps textured
objects opaque. Tests updated to the evidence-based semantics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adapted to Task-1 RE: position is a direct V24 set (not ping-pong); the
oscillating channels are src-rect scroll (0x231/0x239) and color glow (0x232).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One-unit scope for the gfx animation cluster (0x1fd, 0x21c-0x243): channel
model + faithful gfx_object_anim_interpolate port on FrameClock (fixes 'mach 5')
+ spritesheet cell selection. Grounded in the recovered dispatch table
(handler(op)=ctx[0x26c93+op]; Kelebek labels are drift) + the reversed
interpolator. Bounded RE Task-0 with known handler addresses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0x202/0x203 now route through GfxState.SetObjectColor (sets HasColor);
SnapshotVisibleObjects resolves Alpha/Tint/BlendKind. Drops the stale
'alpha deferred' trace stub — alpha/tint is now consumed by the compositor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TDD plan (BlendMath -> RenderObject resolution -> host colorkey/alpha/tint blit
-> surfaceless fade fill -> docs). Records the reversed colorkey format and the
0x202/0x203 color workers in engine-re.md (Ghidra annotated+saved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hybrid architecture (engine resolves RenderObject blend plan, host blits).
Grounded in fresh Ghidra RE of gfx_object_composite/blit + the 0x202/0x203
color workers (renamed+plate-commented, saved). Includes a bounded RE Task-0
(colorkey format, blend-mode source, color/alpha anim coupling).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design artifacts for the merged frame-stepped VM work (throttle the Godot VM
to a per-frame op budget). Probes measured the native ~1788 ops/sec cadence
and uncapped D3D9 Present that motivated the wall-clock-op-rate approach.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prior-session WIP: RE findings on the engine frame cadence (engine-re.md,
phase-a-slice-plan.md, tools-reference.md) and a null-guard so headless
--shot-sequence advances without a rendered viewport texture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The frame-paced-sleep slice did NOT make the opening burst animate (only the
one-shot dramatic pauses). Correct the canonical result (phase-a-slice-plan),
the RE doc (engine-re), the opcode source+generated ref (opcodes.toml 0xc8),
and add correction banners to the point-in-time spec/plan. Also folds in the
diagnostics + headless halt-at-wait results into phase-a-slice-plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The headless divergence that sent us chasing a phantom 'sleep' spin: op 0x72
wait-for-input was a no-op headless, so a run plowed past all 166 of a scene's
prompts into code no real playthrough reaches (SC0000 -> the name-entry poll
loop, spinning sleep 1 493k x to STEP-LIMIT). That path is a fiction.
Fix: VmOptions.HaltAtWaitForInput -> the VM halts (reason 'wait-for-input') at
0x72. run/play default to faithful (SC0000 now halts at ~402 steps, 0 sleeps,
matching the real run's path to the first prompt); --plow opts into the old
walk-every-page coverage. sweep stays plow by default (dialogue oracle, 284/13
unchanged); --halt-at-wait makes all 297 scenes halt cleanly at their first
prompt (0 STEP-LIMIT). Godot unaffected (really blocks on input; flag false).
Engine 58/58 (2 new); sweep default 284/13 unchanged; Godot selftest OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>