Commit Graph

345 Commits

Author SHA1 Message Date
gamer147
9b3b922c30 Merge: gfx-object-manager RE — drift root cause + engine-dump unlock
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>
2026-07-07 00:10:58 -04:00
gamer147
6233f58e6c 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>
2026-07-07 00:10:45 -04:00
gamer147
16fe754422 feat(frida): capture_gfx_objects + finding — drift is state-divergence
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>
2026-07-07 00:04:49 -04:00
gamer147
fbc78b0c00 feat(frida): probe_handlers — confirms interpreter runs from module 0x400000
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>
2026-07-06 23:51:18 -04:00
gamer147
70db77af46 fix(frida): dump_engine — use ptr.readByteArray (frida 17), skip system DLLs
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>
2026-07-06 23:43:54 -04:00
gamer147
f16e5bd7b8 feat(frida): dump_engine.py — dump unpacked engine code from live process
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>
2026-07-06 23:33:02 -04:00
gamer147
986f2080a0 docs: SYS4AB.BIN = XOR-0xFF(AGE.EXE), same packed binary (dead end for static RE)
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>
2026-07-06 23:29:37 -04:00
gamer147
2951425108 docs(opcodes): 0x215 = native graphics-object query (root cause of gfx drift)
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>
2026-07-06 23:21:08 -04:00
gamer147
d177a31185 Merge: A2b graphics geometry (0x208 keystone + blit compositor)
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>
2026-07-06 23:06:39 -04:00
gamer147
5f5fb072cd docs(a2b): record graphics-geometry result + deferred anchor-record drift
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:05:56 -04:00
gamer147
78199e9b8c feat(a2b): screen-backbuffer blit compositor + real GetTextureSize
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>
2026-07-06 23:02:58 -04:00
gamer147
de5eb6562e feat(a2b): BmpHeader.ReadDims + gfx diagnostic (headless geometry oracle)
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>
2026-07-06 22:50:25 -04:00
gamer147
ea0f1ddc7a feat(a2b): implement 0x208 get-texture-size (geometry keystone)
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>
2026-07-06 22:48:10 -04:00
gamer147
e96b1a6fef docs(a2b): implementation plan — graphics geometry (0x208 + blit compositor)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:43:35 -04:00
gamer147
47e4db71e2 docs(a2b): design — graphics geometry (0x208 keystone + blit compositor)
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>
2026-07-06 22:38:11 -04:00
gamer147
ee70beb93d feat(a2b): audio — wire play-bgm/play-voice, fix BGM addressing
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>
2026-07-06 22:26:54 -04:00
gamer147
10f6656c2d feat(a2b): first-pass texture render — full-screen event-CG layer
Wire asset resolution into a live Godot render. VM executes set-texture ->
ResourceMap resolves (scene,resId) -> files[section_base+resId] across all
archives -> pre-converted BMP -> composite behind the dialogue. The full-screen
event-CG layer (EV052*) renders end-to-end from the executed bytecode.

- Age.Engine/Sys4/ResourceMap.cs: Resolve + BMP TexturePath; Paths: asset JSONs
- GodotAdvHost: create/set/draw-texture -> TextureRect in a _stage layer
- IHost.DrawTexture + VM dispatch extended with dst x/y (draw-texture args 7/8)
- project.godot 800x600; convert_agf.py all-archive + --scene batch
- engine 8/8, C# --selftest still byte-matches vm0 trace (VM behaviour unchanged)

Known limitations (next chunk = graphics geometry/blend subsystem):
- sprites + BG* via the CG-load subroutine get garbage dst/size — native ops
  stubbed (0x208 get-texture-size + sprite position/anim chain)
- AE* fades draw opaque/instant (no alpha); no chromakey
- slot model approximates the game's immediate-mode blit-onto-slot-0 canvas
- AGF pre-converted to BMP offline (runtime decoder deferred)
See docs/phase-a-slice-plan.md (A2b section).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 21:22:42 -04:00
gamer147
a61c0c9abd feat(assets): solve asset resolution (SYS4INI per-scene section manifest)
resId -> files[section_base(scene) + resId]. SYS4INI's file list is
sectioned, one per scene (SCxxxx.BIN + its cross-archive asset manifest);
file_number is the index within the section. Unified for set-texture,
play-bgm, play-voice. Fully static/general -> no per-scene capture.

- tools/parse_sys4ini.py: SYS4INI (S4IC422, LZSS) -> build/asset-index.json
- tools/resolve_asset.py: sections + (scene,resId) resolver -> build/asset-sections.json
- validated: 97% structural, SC0000 17/17 vs Frida, 586/595 captured loads
- opcodes.toml: set-texture/create/draw-texture, play-bgm/voice enriched (frida-grounded)
- Frida tooling (capture_load_order all-archive, correlate_scope, ...) + vm0 --settex
- docs: asset-resolution-re (step2 SOLVED), global-memory-re (shelved), tools-reference

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:48:22 -04:00
gamer147
b92e815850 feat(a2b): asset-RE tooling + findings; render blocked on asset resolution
- tools/convert_agf.py (AGF->BMP stills), tools/frida/ (capture harness + README; Frida 17.15.3)
- docs/asset-resolution-re.md: foundational RE steering doc (resId->file; graphics+audio; not
  machine-verifiable -> Frida ground truth + human oracle). RE plan: parse SYS4INI, crack resId->name,
  backend render (A2b plan Tasks 3-5), audio, movies.
- Finding: SC0000 bg = slot-0 slideshow (res 0x23 first); resolution opaque (CGINIT not a name map,
  SYS4INI S4IC needs RE, Frida file-I/O noisy/memory-mapped, opening mixes MPEG movies).
- slice plan updated; texture ops already engine-driven (Task 1, prior commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:36:17 -04:00
gamer147
d2d798bbac feat(a2b): promote create/set/draw-texture to IHost methods (trace parity kept)
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>
2026-07-06 15:49:07 -04:00
gamer147
ef2cd03af2 docs(a2b): implementation plan for the engine-driven background layer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:44:07 -04:00
gamer147
5af5622601 docs(a2b): spec for the engine-driven background layer (SC0000)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:37:05 -04:00
gamer147
783b771020 fix(a2a): advance on mouse click (use _Input; root Control ate clicks in _UnhandledInput)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:09:23 -04:00
gamer147
78b7d92e93 feat(a2a): CJK font for the message window; mark A2a done in the slice plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:52:40 -04:00
gamer147
fcaa4caf47 feat(a2a): worker-thread ADV host + blocking wait-for-input + headless self-test
SELFTEST OK: 186 lines match vm0 trace; real SemaphoreSlim gate + CallDeferred exercised headless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:51:07 -04:00
gamer147
d3286cf96b feat(a2a): Godot 4.7 .NET project scaffold referencing Age.Engine (headless smoke)
SMOKE: Paths resolves to age-reimpl inside Godot; Age.Engine parses SC0000 in-process.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:49:57 -04:00
gamer147
b668c83de4 feat(a2a): add IHost.WaitForInput hook (wait-for-input 0x72); trace parity preserved
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:48:46 -04:00
gamer147
e42655a849 docs(a2a): implementation plan for the interactive dialogue loop
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:42:15 -04:00
gamer147
26dcf001f2 docs(a2a): spec for the interactive dialogue loop (Godot + Age.Engine)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:31:57 -04:00
gamer147
afc9511270 docs: mark A1 (C# VM core) done in the slice plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:01:33 -04:00
gamer147
a8a91e48f6 feat(a1): CLI (run/trace) + vm0.py --trace + per-scene differential test (A1 green)
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>
2026-07-06 13:55:11 -04:00
gamer147
121686fbb1 feat(a1): VM core (operands+pointer semantics, handlers, emit-cap) + RECOVER
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:53:03 -04:00
gamer147
0cb83fcb62 feat(a1): SYS4 container parser (header + code decode + data_array_end shrink)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:51:32 -04:00
gamer147
f3fe8ffe58 feat(a1): SYS4 XOR-FF cp932 string codec
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:50:35 -04:00
gamer147
a704765ea0 feat(a1): .NET 8 solution scaffold + Model + OpcodeTable loader
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:49:02 -04:00
gamer147
a50b7f600a docs(a1): implementation plan for the C# VM core
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:45:05 -04:00
gamer147
a6cd43ce74 docs(a1): spec for the C# VM core (headless, trace-diff vs vm0.py)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:32:42 -04:00
gamer147
0ffb9620a3 docs(opcodes): retire superseded opcode files; update structure + doc pointers
- rm vm-map/opcodes-himegari.json, vm-map/himegari-opcode-notes.md (folded into opcodes.toml; in history)
- PROJECT-STRUCTURE.md: opcodes.toml single-source-of-truth + generated artifacts + convention
- repoint dead references in vm-mapping-plan/sys4-format-notes/himegari-port-reference/phase-a-slice-plan

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:16:33 -04:00
gamer147
9f140fda83 feat(opcodes): migrate to opcodes.toml as single source of truth; regenerate artifacts
- 248 opcodes seeded from Kelebek + corpus arg-types; 23 inferences migrated
- source = provenance (frida/unicorn methods = inference + confirm_by, not grounded)
- 0x90/0x97 enriched with depends_on + details
- differential-verified: SC0830/MENU disasm byte-identical, vm0 --test PASS, sweep 282/294

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:11:33 -04:00
gamer147
cee746250b feat(opcodes): emit opcodes.json, opcode-reference.md, coverage
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:04:35 -04:00
gamer147
9f37a6c31e feat(opcodes): emit drop-in age_opcodes_himegari.py shim; wire --build/--lint
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:03:33 -04:00
gamer147
5d2efa1249 feat(opcodes): bootstrap seeds 248 skeletons from Kelebek + corpus arg-types
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:02:28 -04:00
gamer147
503eb70359 feat(opcodes): linter (dangling-ref, confidence-ceiling, vocabulary)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:01:20 -04:00
gamer147
9f15aa0336 feat(opcodes): data model + loader for opcodes.toml
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:00:31 -04:00
gamer147
463877773c chore: initialize age-reimpl repo
Reverse-engineering + open reimplementation workspace for Eushully's AGE/SYS4
engine (first target: Himegari). The repo root is age-reimpl/; the original game
install and the extracted ALF data are siblings outside the repo and are never
tracked. build/ (derived corpora) is gitignored and regenerated by the tools.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:58:30 -04:00