docs(a2a): spec for the interactive dialogue loop (Godot + Age.Engine)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
117
docs/superpowers/specs/2026-07-06-a2a-godot-dialogue-design.md
Normal file
117
docs/superpowers/specs/2026-07-06-a2a-godot-dialogue-design.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user