Commit Graph

143 Commits

Author SHA1 Message Date
gamer147
b9ca284f90 feat: port anim interpolator (ping-pong spritesheet cell + color glow) on nowMs
BlendMath.PingPong + SnapshotVisibleObjects(nowMs); no-arg overload keeps
headless callers deterministic (parity held, 76/76 + sweep 284/13).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:05:00 -04:00
gamer147
7ca51bfdf3 feat: add src-rect (spritesheet) + animated-color channel fields/mutators
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>
2026-07-08 22:02:46 -04:00
gamer147
143fe8258c feat: resolve per-object alpha/tint/blend into RenderObject (colorkey retained)
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>
2026-07-08 19:06:34 -04:00
gamer147
4ee7a26e3f feat: add BlendMath (colorkey match + ARGB unpack)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:04:12 -04:00
gamer147
cd93ebae2e feat: add IHost.FrameYield per-opcode hook (no-op headless, parity held)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:29:50 -04:00
gamer147
82492d8a6c feat: add FrameClock (virtual clock + per-frame op budget)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:27:41 -04:00
gamer147
791ea2ca0f fix(vm): faithful headless — halt at wait-for-input instead of plowing
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>
2026-07-08 10:45:32 -04:00
gamer147
9ccfc43959 feat(diag): aggregating + filtered trace sinks (histogram, op-filter, per-script)
The existing trace framework only had a flat text formatter, so every question
became 'dump millions of lines, then grep'. This session that cost a long wrong
detour. Add, all observe-only (parity preserved):

- HistogramTraceSink: execution counts per opcode AND per call-site (script:pc)
  with a sample operand. Dumped sorted after the run. This is what instantly
  showed the 493k headless 'sleep's are INPUTNAME.BIN:0x1c3 (a name-entry poll
  loop), not the opening.
- TraceSinkBase: tracks the frame stack -> attributes each step to its REAL
  script (nested call-script frames included) = the 'which script is this pc in?'
  answer a bare step trace can't give.
- TextTraceSink: op-filter (--trace-ops sleep,draw-texture,...) + script:pc tags.
- CompositeTraceSink: fan-out (text + histogram + Godot's call-script queue).
- OpcodeTable.ByLabel: mnemonic -> opcode for --trace-ops.
- CLI: --trace-histogram, --trace-ops, robust --trace-file (mkdir -p).
- Godot: --trace-histogram <file> profiles the REAL run (headless flow diverges:
  real run to page 1 is 562 steps / 0 sleeps vs headless 2M steps / 493k sleeps).
- Also: --sleep-scale <f> debug knob to slow the paced opening for inspection.

Engine 56/56 (4 new); sweep parity 284/13; Godot builds + dogfooded end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:50:07 -04:00
gamer147
8ac9e6675b fix(gfx): serialize GetOrCreate/Register/Release on _lock (compositor race)
Once sleep paces the VM thread, the main-thread compositor's SnapshotVisibleObjects
truly overlaps VM-thread _objects/_registry writes. GetOrCreate/Register/Release were
unlocked -> 'Destination array is not long enough' under concurrent enumeration.
_lock is re-entrant so BindDraw/EraseRange (already locked) stay correct. 52 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:01:13 -04:00
gamer147
5f245d4cf4 feat(vm): IHost.Sleep seam + dispatch 0xc8 (headless hosts no-op; parity held)
- IHost.Sleep(long duration); VM case "sleep" forwards the raw operand.
- 8 non-Godot hosts no-op Sleep; RecordingHost records it.
- SleepDispatchTests (51 green); sweep unchanged 284 exit / 13 STEP-LIMIT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 08:59:52 -04:00
gamer147
bcb95f3bb5 feat(gfx): surface anim channel to the compositor via RenderObject.AnimState
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:39:18 -04:00
gamer147
0495628b99 feat(gfx): dispatch anim-start 0x234 + set-anim-clock 0x238 (revised global-clock model)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:38:15 -04:00
gamer147
328c25bc56 feat(gfx): label + dispatch set-anim-transform 0x21e/0x220 into GfxState
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:37:28 -04:00
gamer147
6339b93c72 feat(gfx): GfxState animation-channel model (per-object transform + global clock)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:34:52 -04:00
gamer147
962e55b43d fix(gfx): op 0x215 query registry is separate from the geometry store
The retained-mode "2nd CG renders off-screen" bug: GfxState conflated two
distinct native structures. It assigned a fabricated AcquireSlot() slot on
every GetOrCreate (called by all geometry/draw ops) and returned it from
QuerySlot (op 0x215). But Ghidra (gfx_op_0x215_register_query @0x42a0b0 /
gfx_op_0x1a2_registry_insert @0x42d360) shows 0x215 does map.find(handle) over
a registry populated ONLY by op 0x1a2 -- it never allocates a slot.

So a CG handle (never 0x1a2-registered) read back as "existing", took the
existing branch of label_12649, ran get-texture-size on the wrong slot (0),
got size 0, and computed dst = pos(0,0) - (w/2,h) = (-400,-600) -> off-screen.
The real engine returns -1 -> the fresh branch -> anchor from the INIT2 arrays
-> dst=(0,0).

Fix: GfxState keeps a separate _registry (HashSet) populated only by
Register() (op 0x1a2); QuerySlot returns the handle if registered else -1, and
no longer consults the geometry store or invents slots. Drop AcquireSlot /
GfxObject.Slot / the free-list.

Verified: Age.Cli gfx --boot SC0000.BIN -> all event CGs dst=(0,0), zero
(-400,-600) draws; Godot --boot pages 1/2/4 render opening CGs full-screen;
engine 44/44; sweep parity 284 exit / 13 STEP-LIMIT unchanged.

Docs: engine-re.md (query-registry-vs-geometry-store section), opcodes.toml
0x1a2/0x215 rebuilt; Ghidra helpers gfx_registry_map_find/hash_insert
annotated + saved. Tests rewritten to the native contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:09:11 -04:00
gamer147
6775be2ac5 feat(gfx): surface+object render model (replaces flat layers)
GfxState: SurfaceStore (slot->{resId,colorkey} from create/set-texture) + object
SourceSlot/SrcRect/Visible (from draw-texture bind); SnapshotVisibleObjects returns
visible objects in ascending-handle order (=z-order) with their live surface resolved.
VM set/create/draw-texture wired to it. Oracle dumps visible objects. Erase removes the
object from the registry (faithful). Engine 44 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:28:47 -04:00
gamer147
6dda931bc0 feat(gfx): draw-texture records a retained layer in GfxState
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 19:46:21 -04:00
gamer147
fc9e46ee85 feat(gfx): retained DrawLayer list in GfxState (thread-safe)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 19:45:28 -04:00
gamer147
62fa3231bc fix(gfx): 0x1f7 is a registry ERASE, not a create
RE correction: op 0x1f7's worker gfx_registry_erase_range (@0x47d8b0) loops
gfx_registry_erase over [handle, handle+count) — a teardown, not a create.
Objects are created lazily by the geometry SET ops (gfx_object_get_or_create).
GfxState.EraseRange + VM case + tests; opcodes.toml renamed gfx-elem-create ->
gfx-elem-erase; Ghidra handler + workers annotated. Booted SC0000 CG geometry
unchanged for the working CGs (no regression). Engine 40 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 19:05:31 -04:00
gamer147
85e821a4f4 feat(gfx): colored-draw ops store packed ARGB; alpha blend deferred + guarded
0x202/0x203 pack (alpha,color) via GfxState.PackColor onto the object; the actual
alpha/additive blend is deferred and surfaced once through the trace sink (observe-
only, parity held). Full suite 37 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 18:02:59 -04:00
gamer147
648ccca2e7 feat(gfx): execute gfx command-buffer query/set/lifecycle ops in the VM
Gfx property on VirtualMachine + dispatch cases for the query ops (0x215/0x216/
0x218/0x21a), set ops (0x217/0x219/0x1ff/0x212/0x213) and lifecycle (0x1a2/0x1f7/
0x1fa). Query ops write results to operands like get-texture-size; set ops mutate
GfxState. Round-trip + distinct-slot tests pass; full suite 36 green (parity held).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:48:31 -04:00
gamer147
f66b5e7f16 feat(gfx): GfxState host-agnostic command-buffer model
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:33:51 -04:00
gamer147
527fba02e8 feat(cli): --trace flag streams engine diagnostics (run/play/sweep)
Also gate Stub events under TracingSteps: stubbed ops are per-instruction
frequency (0x258/0x259 stmt markers en masse), so --trace stays a clean
high-level flow view; --trace-steps shows step+stub detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:25:59 -04:00
gamer147
2a71847a5d refactor(hosting): drop CallScript/OnStub from IHost (now trace events)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:22:40 -04:00
gamer147
e010393b8a feat(diagnostics): VM emits trace events + CallScriptDispatches stat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:21:28 -04:00
gamer147
10cf79a41b feat(diagnostics): ITraceSink seam + TraceEvent + Null/Text sinks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:19:40 -04:00
gamer147
6604a1fa54 Test on synthesized scenes with full handling, not crippled real scenes
- ScriptAssembler (Age.Engine/Sys4): assemble code+strings -> Script (inverse of
  Sys4Loader; also Phase-D modding-assembler groundwork).
- SyntheticSceneTests: deterministic show-text/wait/nested-call-script/shared-global
  scene run with call-script handling ON.
- WaitForInputTests: reworked onto a synthesized two-page scene (was: SC0000 stub=186).
- RecoverTests: full call-script handling via a no-op subroutine double (isolates
  RECOVER's ISA semantics from real subroutines' game-state deps).
- Retire TraceDiffTests: it matched the C# VM to vm0.py's stubbed-call-script trace;
  vm0.py is retired from oracle duty, and we no longer gate handling to keep it matching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:45:42 -04:00
gamer147
6b3d90182e Wire Sys4ScriptProvider into CLI run/play/sweep; validate real subroutine execution
Integration: ADDILL executes ADDILLSUB + CALCREVISE and returns to its own exit.
Sweep (execution on): 284/297 exit clean, 13 STEP-LIMIT (input/state-gated ADV
scenes that now spin headless once subroutine global-writes drive their loops —
state divergence, not a call-script bug; 0 depth-cap, 0 unresolved, 0 crashes).
Removed the dead _halted field (halt propagates via FrameOutcome).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:18:30 -04:00
gamer147
4c27707d57 Execute call-script: nested frame, shared globals, return to caller
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:13:57 -04:00
gamer147
3f9117363c Refactor VM to ExecFrame + tag emitted lines with source script (no behavior change)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:11:50 -04:00
gamer147
c8c79c9831 Add IScriptProvider + Sys4ScriptProvider (call-script id -> Script)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:09:50 -04:00
gamer147
f6d1384b00 feat(phase-b): GameSession snapshot save/load (JSON)
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>
2026-07-07 00:26:23 -04:00
gamer147
b762b3d1c0 feat(phase-b): cross-scene state — GameSession + play runner
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>
2026-07-07 00:19:11 -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
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
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
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
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