# Design: A2a — Interactive Dialogue Loop (Godot + Age.Engine) Status: **approved (design)** · Date: 2026-07-06 Related: `docs/phase-a-slice-plan.md` (A2), `docs/remake-architecture-and-roadmap.md`, the A1 engine (`engine/Age.Engine`), `docs/superpowers/specs/2026-07-06-a1-csharp-vm-design.md`. ## Problem / goal A1 delivered a headless C# VM byte-identical to `vm0.py`. A2a is the first *presentation* slice: stand up a **Godot 4.7 (.NET)** project that references `Age.Engine` in-process, make the VM **suspendable**, and play one CLEAN scene (**SC0000**, 186 lines) as a message window whose text **pauses at `wait-for-input` (0x72) and resumes on a click**. It proves the two load-bearing A2 unknowns — (1) Godot .NET can host and run our engine in-process, and (2) the suspend/resume interactive loop — with the validated VM core left essentially unchanged. Background art, voice, choices, and `call-script`/state are **A2b**. ## Non-goals (→ A2b / later) Background/AGF pipeline, `play-voice`/`play-bgm`, choices (buttons → VM), `call-script`/state seeding to enter richer scenes, `set-font`, remaining draw/audio effectful ops, exported-build data packaging, UX polish (text speed, backlog, skip). A2a is the interactive VM↔Godot loop only. ## Prerequisites (verified 2026-07-06) - Godot **4.7-stable mono** at `S:/Godot/Godot_v4.7-stable_mono_win64/` (`GodotSharp/` present; `--version` → `4.7.stable.mono.official`). Console exe used for headless runs. - `build/opcodes.json` and `build/vm0-trace.json` exist (from A1); `../extracted/DATA1/SC0000.BIN` present. - `Age.Engine` targets `net8.0` (Godot 4.7 .NET also targets .NET 8) → compatible. ## Architecture ``` godot/ Godot 4.7 .NET project (config_version=5) project.godot main scene = Main.tscn; dotnet/project config Himegari.csproj Godot.NET.Sdk; net8.0; ProjectReference ../engine/Age.Engine Main.tscn Control(root) > RichTextLabel + Label(status) Main.cs owns the VM worker thread + UI methods GodotAdvHost.cs : Age.Engine.Hosting.IHost interactive backend ``` - **In-process, no IPC:** the Godot game assembly references `engine/Age.Engine/Age.Engine.csproj` directly. Data is resolved by the existing `Age.Engine.Sys4.Paths` (walks up to `age-reimpl/`; the `godot/` project is inside it, so `Paths.OpcodesJson` / `Paths.Scripts()` work in the editor and in headless dev runs). *(Exported-build data packaging is A2b/C.)* ### One VM-core change (kept minimal to preserve trace parity) Add to `IHost`: `void WaitForInput();`. In `VirtualMachine.Step`, split `wait-for-input` out of the no-op group: ```csharp case "wait-for-input": _host.WaitForInput(); return pc + 1; ``` `CaptureHost.WaitForInput()` is a **no-op**, so `Steps`/`Emitted` are unchanged and **A1's trace-diff and RECOVER stay green** (calling an empty method changes nothing the trace observes). This is the entire core change; all suspend/resume logic lives in `GodotAdvHost`. ### Threading model (background thread + blocking host) `Main.cs` runs `vm.Run()` on a `Task` (worker thread). Ownership is clean: the worker owns VM state, the main thread owns UI, and `WaitForInput` is the only rendezvous. `GodotAdvHost` (constructed with the `Main` node and an `autoAdvance` flag): - `ShowText(off, text)` → `main.CallDeferred(Main.MethodName.AppendLine, text)` (append on main thread); also record `(off, text)` into a `Captured` list (for the self-test). - `WaitForInput()` → if `autoAdvance` return immediately; else `main.CallDeferred(Main.MethodName.PageBreak)` then `_gate.Wait()` (a `SemaphoreSlim(0,1)`); on resume, `main.CallDeferred(Main.MethodName.ClearPage)`. - `SignalInput()` → `_gate.Release()` (called from the main thread on click). - `CallScript`/`OnStub` → record/ignore (A2a stubs them like A1). `Main.cs`: - `_Ready()`: parse `OS.GetCmdlineUserArgs()` for `--selftest`; load `OpcodeTable` + SC0000 `Script` via `Paths`; build `GodotAdvHost(this, autoAdvance: selftest)`; start `_task = Task.Run(() => { _vm.Run(); _done = true; })`. - `_UnhandledInput(e)`: on `ui_accept` or a left mouse click, `_host.SignalInput()`. - UI methods (called via `CallDeferred`): `AppendLine(string)` appends to the `RichTextLabel`; `PageBreak()` shows a "▼ click" indicator; `ClearPage()` clears the label; `ShowEnd()` shows "— end —". - `_Process()`: when `_done` flips true, call `ShowEnd()` once; in **selftest** mode, compare `_host.Captured` offsets to the expected SC0000 sequence and `GetTree().Quit(exitCode)`. ## Data flow (one page) worker `vm.Run()` → N× `ShowText` (each `CallDeferred(AppendLine)`) → `wait-for-input` → `WaitForInput()` shows ▼ and blocks the worker → user clicks → `_UnhandledInput` → `SignalInput()` releases → worker clears the page and continues → … → `exit` → `_done` → `ShowEnd()`. ## Validation - **Headless self-test (auto-verifiable, the A2a gate):** run `Godot_v4.7-stable_mono_win64_console.exe --headless --path godot -- --selftest`. `Main` runs SC0000 on the worker thread with `autoAdvance` (WaitForInput returns immediately), still marshalling `AppendLine` via `CallDeferred` (so the thread + deferred path is exercised headlessly), waits for `_done`, then asserts `_host.Captured` offsets equal `build/vm0-trace.json["SC0000.BIN"]["offsets"]` (186 lines) and quits `0`/`1`, printing `SELFTEST OK`/`SELFTEST FAIL: …`. This proves Godot .NET hosts Age.Engine, the worker-thread + `CallDeferred` marshalling works, and the emitted text matches the trusted oracle. - **A1 regression:** `dotnet test engine/AgeEngine.sln` → still 6/6 (WaitForInput no-op in CaptureHost). - **Manual visual (human):** open `godot/` in the Godot 4.7 .NET editor (or run non-headless) and click through SC0000 — text appears page by page, pauses at ▼, advances on click, ends cleanly. The suspend/resume timing under a real window is the part only a human can confirm. ## Risks - **Godot .NET project scaffolding by hand** — `project.godot` + `.csproj` must be correct for `godot --headless` to build the C# assembly. Mitigation: generate via the Godot editor's C# setup if the hand-authored files fail to build; the plan verifies with a headless build before wiring logic. - **`CallDeferred` method binding** — deferred calls target `Main` methods by name; they must be `public` (or `[Signal]`/source-gen `MethodName`) on the `Node`. Verified by the self-test. - **Headless drivers** — use `--headless` (no audio/video driver needed); the self-test avoids real input. - **Thread vs Godot lifetime** — if the scene exits while the worker blocks in `WaitForInput`, release the gate in `_ExitTree()` and guard `CallDeferred` after tree exit. Covered in the plan. - **Cross-thread visibility** — `_done` is `volatile` (worker writes, `_Process` reads); `_host.Captured` is only read after `_done` is observed true, so the completed write is safely published. No lock needed beyond that ordering. - **`Paths` in an exported build** — dev-only for A2a (resolves via the repo tree); packaging is deferred. ## Out of scope (A2b and beyond) Background via `AGF2BMP2AGF.exe` → texture, `play-voice`/`play-bgm` (OGG), choices → UI buttons feeding VM globals, `call-script`/state seeding to unlock richer/EMPTY scenes, `set-font`, remaining draw ops, export packaging, and ADV UX (text speed, backlog, auto/skip).