build/globals.json is generated (build/ is gitignored, regenerable via
globals_build.py --build); docs/global-reference.md is the tracked human view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Curated single-source global registry modeled on opcodes.toml; static
story-flag discovery miner; sys4load labels from merged build/globals.json.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
godot -- --seed 0xa57=1 seeds initial global state in the ADV frontend (e.g.
Lily's form-gated voiced dialogue plays live; forms A/B/C = 0xa57/0xa58/0xa59).
Additive: no seed = unchanged, selftest OK. Also documents the engine CLI
(run/trace/audio/gfx/play/sweep) + Godot frontend flags in tools-reference.md
(they lived only in memory/commits).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sweep [--boot] [0xADDR=VAL...] runs each scene with and without the seeds from
the same baseline and reports which scenes' dialogue changes. Maps a story flag's
reach across the corpus. Finding: the Lily form-A flag 0xa57=1 changes dialogue
in 34/297 scenes (SC0000 186->229, SC0040 111->156, ...) — a pervasive ADV lever,
validating the state-divergence finding at scale. Engine 18/18.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Frida-free foundation for cross-scene playthrough and state seeding:
- GameSession: persistent global bank carried across scenes (VM untouched, parity held).
- play [--boot]: run a scene sequence carrying state; --boot runs the 9 *INIT
data scripts into the bank (23646 globals) so scenes see real skill/item/unit data.
- Snapshot save/load (--state/--save-state): capture an expensive booted state once,
reuse it; foundation for save-files.
- sweep [--boot]: corpus-scale validation (matches vm0.py: 294 exit + 3 LOOP).
Findings operationalized: seeding the Lily form flag 0xa57=1 takes SC0000 186->229
lines (state divergence, headless). Data-boot is regression-free but doesn't change
ADV flow — story-state flags drive that. Engine 18/18; Godot selftest OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runs every SC/SP scene through GameSession (fresh or booted-from-snapshot) and
reports halt distribution + line counts. Unbooted matches the vm0.py A0 baseline
(294 exit + the same 3 LOOP scenes) — cross-validates the C# VM at scale.
FINDING: booting the *INIT data tables gives byte-identical results (regression-
free) but does NOT change ADV dialogue flow — *INIT feeds gameplay; ADV branches
key on story-state flags (chapter/form/choices) from the progression layer, not
*INIT. So story-state seeding (e.g. the Lily form flag) is the ADV-unlock lever.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToJson/FromJson round-trip the persistent global bank; play --save-state <file>
persists it, --state <file> restores it — so an expensive booted state (23646
globals) is captured once and reused without re-running *INIT, and it's the
foundation for real save-file work. Tests: unit round-trip + booted-state
survives snapshot (skill data intact). Engine 18/18.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All 9 *INIT scripts run clean and populate the game's data tables into the
global bank. play --boot prepends them before scenes so scenes see real
skill/item/unit/map/stage state (23646 globals for SC0000). Test cross-checks
the VM's SKINIT population against the static extraction (build/data/SKINIT.json:
skill 0 '飛行'@0x23a3, G[0xa6e5b]=30) — validates both the VM and extract_init.
Engine 16/16.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Persistent global store carried across scenes (the engine's flat global bank),
the substrate for cross-scene flow and state seeding. GameSession seeds a fresh
VM from session state, runs, merges back; VM untouched so trace/selftest parity
holds. Age.Cli play <SCENE...> [0xADDR=VAL] runs a sequence carrying state.
Tests (engine 15/15): state persists A->B; seed visible to scene; SC0000 via
session byte-identical to single run; seeding form flag 0xa57=1 changes behavior.
Demonstrated: play SC0000 0xa57=1 -> 186->229 lines (Lily's form-gated dialogue
executes); SC0000->SC0030 carries 76 globals. Operationalizes the state-
divergence finding. Godot selftest OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverse-engineered the post-opening bg/sprite drift end-to-end:
- Root cause: stubbed 0x215 (native gfx-object query) collapses draws to slot 0.
- Unlocked static analysis of the unpacked engine (dump_engine.py + capstone;
AGE.EXE unpacks in-place at 0x400000; SYS4AB = XOR-0xFF(AGE.EXE) dead end).
- Live capture verdict: the real opening uses zero CG object-records => the drift
is a STATE-DIVERGENCE artifact of the unseeded headless VM, not a missing op.
Fix = Phase B state/choices flow. Native gfx-op modeling deferred (dump in hand).
No engine-code changes (RE tooling + docs only); engine 11/11 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Live capture (esi=engine ctx via operand-fetch, poll object-record array
[esi+0x53d64] stride 120). KEY FINDING: through the full real opening, the record
array holds only 3 persistent UI objects — NO CG objects. The real game does NOT
draw opening CGs via the 0x212-0x21a positioned-object path our headless VM uses;
with state it takes a different (direct) branch. So the bg/sprite drift is a
STATE-DIVERGENCE artifact of the unseeded headless VM, not a missing native op —
the fix is the Phase B state/choices flow (makes label_12649 take the if-branch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
operand-fetch (call 0x41b940) fires ~8500/sec => the VM interpreter executes
from the in-place unpacked module at 0x400000 (NOT the heap copy) => handlers are
hookable by dump address. gfx-family(0x212-0x215)=0 at the title (no CG commands
until a scene runs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dump validated: AGE.EXE is unpacked in-place at 0x400000 in memory (AGF-decoder
landmark @0x474f1f reads real code with the 'BM' 0x4D42 check). Kelebek handler
VAs map directly (VA-0x400000 = file offset). Handler ABI: thiscall (esi=engine
context), operands fetched via call 0x41b940, per-object command-type table at
[esi+idx*120+0x53d88]. 0x215 is part of a native gfx command-buffer manager.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both on-disk engine images are the same packed binary, so native handler code
exists only unpacked in memory. dump_engine attaches, enumerates ranges, and
dumps the AGE.EXE module + large r-x heap regions (chunked) to build/engine-dump/
for offline disassembly (locate 0x215 @ VA 0x421160 via the dispatch table).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decrypted SYS4AB: 0x2c header (S4AB magic + 0x10E000 size + 8-byte key) then
XOR-0xFF payload == AGE.EXE byte-for-byte. Not a patched/unpacked VM. Real
handler code exists only unpacked in the runtime heap (0x62411000). Corrects the
'likely the patched VM' assumption.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigation upgrade: 0x215 queries the native object manager by element
handle-id (0x62455[idx]) and returns the object's slot/status; sign-tested to
drive label_12649's slot-select. Stubbing it collapses all draws onto slot 0 →
the anchor-preserve geometry reads foreign textures → cumulative bg/sprite
drift. Keystone for the graphics object-manager; exact return via unicorn
(handler @0x421160).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opening event-CG sequence now renders correctly. 0x208 get-texture-size
implemented as a real VM op; faithful 800x600 immediate-mode blit compositor;
gfx + --shot diagnostics. Post-opening background/sprite anchor drift is
characterized and deferred to the anchor-record subsystem (next chunk).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Godot host: source images kept per slot with dims read from the BMP header on
the VM thread (synchronous, so the bytecode's geometry math sees real sizes);
draw-texture blits src rect -> dst into one 800x600 canvas in execution order,
shown by a single TextureRect. Selftest byte-parity preserved.
Slot 0 = primary/screen surface: seed its dims to 800x600 (the boot-time
create-texture the single-scene harness skips) and record create-texture(w,h),
so the first CG's anchor-preserve math stays an identity instead of corrupting
the persistent base globals. Fixes the grey-first-CG opening. Verified: SC0000
pages 1/3 composite the opening event CG at (0,0) with dialogue over it.
Also adds a --shot <png> [--shot-page N] dev capture to Main (page-gated on a
VM-thread counter) for headless visual verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BmpHeader.ReadDims reads texture dims from the pre-converted BMP header (VM-
thread-safe, no pixel decode) — the data source for GetTextureSize. `Age.Cli gfx
<SCENE>` runs a scene and dumps set-texture/get-texture-size/draw-texture in
order with resolved file + computed geometry, mirroring the `audio` diagnostic.
Confirms 0x208 now yields real dims (800x600/800x800/800x227) and sane UI
geometry; surfaces the first-load anchor edge (unseeded state, Phase B).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote 0x208 from stub to a real VM op that writes the loaded texture's
width/height into its two output globals, via new IHost.GetTextureSize. This is
the single native primitive the CG-load subroutine (SC0000 label_12649) needs;
all downstream centering/anchor geometry is already computed in bytecode.
Non-Godot hosts return (0,0) so trace/selftest parity holds (engine 9/9 incl.
TraceDiffTests). Godot host gets a temporary (0,0) stub; real impl in the
compositor task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recon of SC0000 label_12649 shows sprite/background geometry is computed in
bytecode; the only missing native primitive is 0x208 get-texture-size. Spec:
implement 0x208 (host returns real image dims) + a faithful 800x600 blit
compositor; defer fades/chromakey/multi-surface. Headless `gfx` diagnostic as
numeric oracle; --selftest/trace parity preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire audio end-to-end (OGG plays natively in Godot; no Frida, no decode):
- IHost.PlayBgm/PlayVoice + VM dispatch for play-bgm(0xbf)/play-voice(0xc4).
Non-Godot hosts no-op them so --selftest + engine 8/8 stay byte-identical.
- GodotAdvHost resolves id->OGG; Main plays via two AudioStreamPlayer nodes
(BGM looping; voice interrupt-on-new).
Key finding (by-ear, systematic-debugging): the two audio ops use DIFFERENT
addressing — the earlier "unified manifest" assumption was wrong for BGM.
- play-voice -> per-scene manifest files[base+id] (offset 0, same as textures).
Confirmed by ear; upgraded med->HIGH.
- play-bgm -> DIRECT LITERAL NAME BGM{id:03d}.OGG, NOT the manifest.
Real game: play-bgm 5->BGM005, 8->BGM008 (manifest gave +1). Proven by
play-bgm 0x23->BGM035 (real standalone track; BGM set skips 030-034) that the
manifest mis-resolved to a graphics entry. Fix is BGM-only:
ResourceMap.BgmPathById; voices/textures unchanged.
Also: Lily's silence root-caused as correct form-gating (G[0xa57/0xa58/0xa59]),
left unseeded by choice (no dummy state). Added diagnostic
`Age.Cli audio <SCENE.BIN> [0xADDR=VAL ...]`. Corrected opcodes.toml (play-bgm
direct-name, play-voice HIGH) + docs/memory (dropped the bogus unified-manifest
/ Frida-BGM006 claims).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runtime finding: SC0000's bg is a slot-0 full-screen slideshow (res 0x23 first), not the
static-path res 0x21. Target corrected to 0x23. CaptureHost/CountHost no-op the new methods.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
C# VM is byte-identical to vm0.py across all 297 SC/SP scenes (offsets+halt+steps);
RECOVER passes; SC0000 = 27994 steps / 186 lines matching the Python prototype.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>