docs: sync living references + roadmap to call-script solved/executing
- tools-reference: run/play/sweep execute call-script (provider); trace/audio/gfx stay provider-less; corrected stale sweep numbers (284 exit/13 STEP-LIMIT) and the Godot --selftest description (synthetic vs headless, not vm0/186); added --scene. - roadmap: call-script marked SOLVED (resolution + execution) throughout; no longer a 'long pole'; per-game registry now automatic from SYS4INI. - phase-a-slice-plan: SC0240 live-demonstration note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -361,6 +361,12 @@ not the depth cap → recursion is bounded correctly).
|
||||
**live headless run** of the same scene — full handling, no frozen golden, no vm0 dependency. Verified:
|
||||
`godot --headless -- --selftest` → "threaded host matches headless (3 lines, full handling)".
|
||||
|
||||
**Verified live in Godot (2026-07-07):** added `--scene <NAME>` to the frontend and a scene-end report
|
||||
of the call-scripts executed as nested frames (collected thread-safely — Godot drops `GD.Print` from the
|
||||
VM background thread). **SC0240 executes 29 call-scripts** (RESETLAND, SETEN, ADDEN, RENDERMAP, SETOBJ,
|
||||
DRAWOBJ, CALCREVISE, LOOK) live in the real runtime; SC0000 renders the opening event CG (windowed).
|
||||
|
||||
**Remaining follow-ups:** optionally give the `audio`/`gfx` CLI diagnostics a provider (they still run
|
||||
provider-less); `decision→scene` (scene chaining) rides this same loader once the SCJUMP
|
||||
decision→scene-id native hop is reversed.
|
||||
provider-less); **engine-level diagnostics** (the next pivot — the engine, not the frontend, should
|
||||
surface script/scene execution + call-script dispatch); `decision→scene` (scene chaining) rides this
|
||||
same loader once the SCJUMP decision→scene-id native hop is reversed.
|
||||
|
||||
@@ -106,8 +106,10 @@ assembly + external data/assets + host hooks; the read-like-C dream is a stretch
|
||||
|
||||
**Two enablers become load-bearing under this goal** (they were "polish" for a port):
|
||||
- **Global-var map** — modders must know what game state a global is to touch it safely.
|
||||
- **Call-script resolution** — needed both to *run* scripts and to *add/replace* scenes. This is now
|
||||
on the critical path, not deferred.
|
||||
- **Call-script resolution** — needed both to *run* scripts and to *add/replace* scenes. **✅ SOLVED
|
||||
(2026-07-07):** `call-script <id>` = a raw index into the SYS4INI file table (native-RE via Ghidra;
|
||||
`docs/engine-re.md`, `name-resolution.md §1`), and the C# VM now **executes** it (loads the target
|
||||
`.BIN` as a nested subroutine frame). Resolution + execution both done; see the status memory.
|
||||
|
||||
---
|
||||
|
||||
@@ -122,8 +124,9 @@ Forces, and thereby de-risks, every core unknown at once:
|
||||
- Port the container parser + VM core to C#.
|
||||
- Implement the ADV effectful ops against Godot: `show-text`, `end-text-line`, `wait-for-input`,
|
||||
`set-font`, `play-voice`, `play-bgm`, `draw-texture`/`create-texture`/`draw-string`, choices.
|
||||
- Resolve **just enough `call-script`** to enter/leave a scene (side-task: `SCJUMP.BIN` decode or a
|
||||
targeted Frida capture — now critical-path).
|
||||
- ~~Resolve **just enough `call-script`** to enter/leave a scene~~ **✅ DONE** — full call-script
|
||||
resolution + execution landed (nested subroutine frames sharing globals). *Scene→scene chaining*
|
||||
(decision→scene) still needs the SCJUMP decision→scene-id native hop.
|
||||
- **AGF → texture** for the one scene's art (side-task; `AGF2BMP2AGF.exe` already on disk).
|
||||
- Treat the classified no-op markers as skips; validate the tentative-no-op ops via the dialogue diff.
|
||||
|
||||
@@ -214,12 +217,13 @@ VM and dead weight on an incorrect one.
|
||||
**Reused for free:** container parser, VM core, backend adapters, the opcode table (the engine ABI
|
||||
is shared across the family), disassembler/assembler, the whole extraction methodology.
|
||||
**Per-game (inherent content work):** the **global-var map** (globals are game-specific), the
|
||||
**call-script registry**, the **data-table layouts** (each game's `*INIT` differs), assets, and any
|
||||
game-specific effectful behavior. Process: point the toolchain at the new game's archives, re-run
|
||||
extraction, rebuild its global map, resolve its call-script registry, author a profile. The long
|
||||
poles are exactly the two enablers (global map + call-script). **This is the core payoff of the VM
|
||||
approach:** the *engine* cost amortizes across all AGE games; only content-mapping recurs — far less
|
||||
than re-coding each game's logic bespoke.
|
||||
**data-table layouts** (each game's `*INIT` differs), assets, and any game-specific effectful behavior.
|
||||
The **call-script registry is no longer a per-game long pole** — it's a raw index into that game's
|
||||
SYS4INI file table, derived automatically by `parse_sys4ini.py` (`build/callscript-names.json`); the
|
||||
resolver is generic. So the remaining long pole is really just the **global-var map**. Process: point
|
||||
the toolchain at the new game's archives, re-run extraction, rebuild its global map, author a profile.
|
||||
**This is the core payoff of the VM approach:** the *engine* cost amortizes across all AGE games; only
|
||||
content-mapping recurs — far less than re-coding each game's logic bespoke.
|
||||
|
||||
### Other engine versions (SYS3 / SYS5) — one app, not many
|
||||
Versions differ in: header (SYS4 `0x3C` vs SYS5 `0x44`), string codec (SYS4 cp932^0xFF vs SYS5
|
||||
@@ -245,8 +249,9 @@ through the same system.
|
||||
**Feasible? Yes — but it is the largest phase of the whole effort**, on the scale of a small ScummVM
|
||||
target. The decoding groundwork substantially de-risks it (we understand the format, 97% of opcodes,
|
||||
the data, a partial global map). Biggest risks, with mitigations:
|
||||
- **`call-script` dispatch entangled in the packed AGE.EXE** → try `SCJUMP.BIN` static decode first;
|
||||
fall back to a targeted Frida capture. Gating for *anything* running.
|
||||
- ~~**`call-script` dispatch entangled in the packed AGE.EXE**~~ **✅ RESOLVED** — cracked statically
|
||||
via the Ghidra dispatch table (no Frida): `call-script <id>` = a raw SYS4INI file index; the C# VM
|
||||
executes it. (SCJUMP was *not* the registry, as first guessed.)
|
||||
- **Effectful-op surface is large and quirk-laden** (esp. SRPG battle/dungeon UI) → ADV-first; defer
|
||||
SRPG; lean on the Frida shortlist.
|
||||
- **AGF graphics** → low risk; `AGF2BMP2AGF.exe` (asmodean) already present.
|
||||
@@ -254,8 +259,9 @@ the data, a partial global map). Biggest risks, with mitigations:
|
||||
- **Toolchain (Python) vs runtime (C#) drift** → share the documented format spec; runtime is
|
||||
canonical.
|
||||
|
||||
**Open questions to resolve early:** exact `call-script` mechanism; how scenes register (needed to
|
||||
*add* content); how much the SRPG layer's rendering diverges from ADV; save layout.
|
||||
**Open questions to resolve early:** ~~exact `call-script` mechanism~~ (solved); how scenes *chain*
|
||||
(the SCJUMP decision→scene-id native hop — needed to sequence and to *add* content); how much the SRPG
|
||||
layer's rendering diverges from ADV; save layout.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -70,21 +70,29 @@ The `engine/` .NET solution (`AgeEngine.sln`) is the runtime VM; `godot/` is the
|
||||
Python, but listed here as the things you *run*. Build: `dotnet build engine/AgeEngine.sln`; test:
|
||||
`dotnet test engine/AgeEngine.sln`. Run a CLI command: `dotnet run --project engine/Age.Cli -- <cmd>`.
|
||||
|
||||
**call-script executes** on the product paths: they inject `Sys4ScriptProvider` (id→`.BIN`, via
|
||||
`build/callscript-names.json`), so `call-script <id>` loads & runs the target as a nested subroutine
|
||||
frame sharing globals. `trace`/`audio`/`gfx` stay **provider-less** (call-script stubbed) — base-ISA /
|
||||
subsystem oracles. Test scenes are **synthesized** via `Age.Engine/Sys4/ScriptAssembler` (see
|
||||
[[testing-synthesize-dont-disable]]: synthesize test data, never disable a feature to keep a golden green).
|
||||
|
||||
| Command | Purpose | Notes |
|
||||
|---|---|---|
|
||||
| `run <file.BIN>` | Execute a scene, print steps + first 20 show-text lines. | `CaptureHost` (headless). |
|
||||
| `trace <out.json>` | Trace every SC/SP scene → offsets + halt + steps (the A1 differential-test oracle). | writes JSON. |
|
||||
| `audio <SCENE.BIN> [0xADDR=VAL…]` | Dump executed `play-bgm`/`play-voice` in order + resolved file. | optional global seeds. |
|
||||
| `gfx <SCENE.BIN> [0xADDR=VAL…]` | Dump executed `set-texture`/`get-texture-size`/`draw-texture` with resolved file + computed geometry (the headless geometry oracle). | optional seeds. |
|
||||
| `play [--boot] [--state <f>] [--save-state <f>] <SCENE.BIN…> [0xADDR=VAL…]` | ★ Cross-scene **state runner**: run a scene sequence carrying persistent globals. `--boot` first runs the 9 `*INIT` data scripts (real skill/item/unit/map/stage state). `--state`/`--save-state` load/persist a JSON snapshot. | `GameSession`. |
|
||||
| `sweep [--boot] [0xADDR=VAL…]` | Corpus-scale validation (unbooted matches vm0.py: 294 exit + 3 LOOP). **With seeds = a story-state explorer**: reports which scenes' dialogue changes ±seed (e.g. form flag `0xa57=1` → 34/297 scenes). | |
|
||||
| `run <file.BIN>` | Execute a script; print steps, show-text count, **call-script dispatch count**, the first 30 lines (each tagged with its source script), and the distinct source scripts. | `CaptureHost` (headless); **executes call-script**. |
|
||||
| `trace <out.json>` | Trace every SC/SP scene → offsets + halt + steps. **Provider-less** (call-script stubbed) = a base-ISA offset dump. | writes JSON. (Was the vm0 differential oracle; vm0 is retired from oracle duty — `TraceDiffTests` removed.) |
|
||||
| `audio <SCENE.BIN> [0xADDR=VAL…]` | Dump executed `play-bgm`/`play-voice` in order + resolved file. | optional seeds. provider-less (stub) for now. |
|
||||
| `gfx <SCENE.BIN> [0xADDR=VAL…]` | Dump executed `set-texture`/`get-texture-size`/`draw-texture` with resolved file + computed geometry (the headless geometry oracle). | optional seeds. provider-less (stub) for now. |
|
||||
| `play [--boot] [--state <f>] [--save-state <f>] <SCENE.BIN…> [0xADDR=VAL…]` | ★ Cross-scene **state runner**: run a scene sequence carrying persistent globals. `--boot` first runs the 9 `*INIT` data scripts (real skill/item/unit/map/stage state). `--state`/`--save-state` load/persist a JSON snapshot. | `GameSession`; **executes call-script**. |
|
||||
| `sweep [--boot] [0xADDR=VAL…]` | Corpus-scale run. **With call-script execution on: 284/297 exit, 13 STEP-LIMIT** (input/state-gated ADV scenes spin headless once subroutine global-writes drive their loops — state divergence, not a bug; 0 depth-cap/unresolved). **With seeds = a story-state explorer**: reports which scenes' dialogue changes ±seed (e.g. form flag `0xa57=1` → 34/297 scenes). | |
|
||||
|
||||
**Godot frontend** (`S:/Godot/Godot_v4.7…`; project = `godot/`). Toolchain: `godot --headless --path godot
|
||||
--import` → `dotnet build godot/Himegari.csproj` → `godot [--headless] --path godot [-- <userargs>]`. It
|
||||
plays `SC0000` from the real bytecode. User args (after `--`):
|
||||
- `--selftest` — headless; asserts the emitted 186 offsets == `build/vm0-trace.json` (parity guardrail). Exits.
|
||||
- `--seed 0xADDR=VAL` (repeatable) — seed initial global state, e.g. `--seed 0xa57=1` unlocks Lily's form-A voiced dialogue (186→229 lines).
|
||||
- `--shot <png> [--shot-page N]` — capture page N to a PNG then quit (headless-verifiable render check / dev screenshots).
|
||||
--import` → `dotnet build godot/Himegari.csproj` → `godot [--headless] --path godot [-- <userargs>]`.
|
||||
Plays the real bytecode with call-script execution on (subroutines run live). `--headless` can't render
|
||||
texture ops (no GPU context) — run windowed for real scenes. User args (after `--`):
|
||||
- `--scene <NAME>` — which scene to play (default `SC0000`), e.g. `--scene SC0240` (executes 29 nested subroutines).
|
||||
- `--selftest` — headless; runs a **synthesized** scene through the thread/suspend/`CallDeferred` plumbing and asserts it matches a live headless run (full handling; no vm0/frozen golden). Exits.
|
||||
- `--seed 0xADDR=VAL` (repeatable) — seed initial global state, e.g. `--seed 0xa57=1` unlocks Lily's form-A voiced dialogue.
|
||||
- `--shot <png> [--shot-page N]` — capture page N to a PNG then quit (dev screenshot). At scene end it also prints the call-scripts executed as nested frames.
|
||||
|
||||
## Asset resolution / graphics
|
||||
|
||||
|
||||
Reference in New Issue
Block a user