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>
This commit is contained in:
gamer147
2026-07-08 10:45:32 -04:00
parent 9ccfc43959
commit 791ea2ca0f
5 changed files with 77 additions and 6 deletions

View File

@@ -207,7 +207,10 @@ public sealed class VirtualMachine
_host.ShowText(off, text);
}
return pc + 1;
case "wait-for-input": _host.WaitForInput(); return pc + 1;
case "wait-for-input":
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
if (_o.HaltAtWaitForInput) { HaltReason ??= "wait-for-input"; return HALT; }
_host.WaitForInput(); return pc + 1;
case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing.
_host.Sleep(Read(a[0])); return pc + 1;
case "end-text-line": case "set-font":

View File

@@ -1,2 +1,10 @@
namespace Age.Engine.Vm;
public sealed record VmOptions(int EmitCap = 2, long MaxSteps = 2_000_000, int CallDepthCap = 64);
/// <param name="HaltAtWaitForInput">When true, the VM halts (reason "wait-for-input") at op 0x72 instead
/// of calling the host and continuing. This is the FAITHFUL headless semantics: with no player to click,
/// "the scene is waiting for input" means stop here — not pretend the click already happened and plow on
/// through every prompt into code no real playthrough reaches (which is what made a headless SC0000 spin
/// 493k× in the name-entry poll loop). Leave false for interactive frontends that really block on input
/// (Godot) and for the dialogue-coverage sweep that deliberately walks every page.</param>
public sealed record VmOptions(int EmitCap = 2, long MaxSteps = 2_000_000, int CallDepthCap = 64,
bool HaltAtWaitForInput = false);