Extract VM input opcode handler

This commit is contained in:
gamer147
2026-08-03 00:17:15 -04:00
parent 5ceea006dc
commit 03ddbf52d6
4 changed files with 200 additions and 131 deletions

View File

@@ -195,6 +195,9 @@ retained history rendering, metadata/voice lookup, and history backlog clearing.
`engine/Age.Engine/Vm/VirtualMachine.AdvServices.cs` owns persistent/active message-skip control, read-skip `engine/Age.Engine/Vm/VirtualMachine.AdvServices.cs` owns persistent/active message-skip control, read-skip
settings and queries, auto-message state/timing, and per-message voice/skip reset opcode dispatch; shared state settings and queries, auto-message state/timing, and per-message voice/skip reset opcode dispatch; shared state
and refresh helpers remain in the VM coordinator because live text and input paths also consume them. and refresh helpers remain in the VM coordinator because live text and input paths also consume them.
`engine/Age.Engine/Vm/VirtualMachine.Input.cs` owns blocking ADV waits, hotspot registration/arming, cursor
resources and virtual position, raw mouse/joystick callback registration and dispatch, action polling, and
physical-input mapping; public host-thread input entry points and shared synchronization remain in the coordinator.
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports

View File

@@ -681,6 +681,13 @@ do not mix mechanical moves with semantic changes.
remain in the VM coordinator because live-text and input paths also consume them. Runtime validation remains remain in the VM coordinator because live-text and input paths also consume them. Runtime validation remains
green. green.
The tenth bounded `VirtualMachine.Step` extraction moved blocking ADV waits, hotspot registration/arming,
cursor resources and virtual position, raw mouse/joystick callback registration and dispatch, action polling,
and physical-input mapping into `engine/Age.Engine/Vm/VirtualMachine.Input.cs`. The top-level dispatcher
retains all labels and aliases at their existing positions and routes them through guarded `StepInput`; public
host-thread input entry points, shared synchronization, and callback service helpers remain in the coordinator.
Runtime validation remains green.
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where **Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
each domain move. each domain move.
@@ -1054,7 +1061,8 @@ Continue step 2 of the **codebase consolidation** maintenance slice: behavior-ne
by the tracked launcher and layered validation driver. With the planned `Main`, `GodotAdvHost`, and `GfxState` by the tracked launcher and layered validation driver. With the planned `Main`, `GodotAdvHost`, and `GfxState`
domains isolated and the audio, movie, surface/texture, retained-object, animation, presentation, ADV-text, and domains isolated and the audio, movie, surface/texture, retained-object, animation, presentation, ADV-text, and
text-history `VirtualMachine.Step` families routed through domain handlers, with ADV skip/auto-message services text-history `VirtualMachine.Step` families routed through domain handlers, with ADV skip/auto-message services
now isolated as well, extract the interactive input/hotspot/cursor callback opcode family next without replacing and interactive input/hotspot/cursor callbacks now isolated as well, extract the sleep/timed-callback sequencing
the proven dispatcher or changing public types, commands, and generated output. opcode family next without replacing the proven dispatcher or changing public types, commands, and generated
output.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
not replace Phase B gameplay validation or the open cross-platform gates. not replace Phase B gameplay validation or the open cross-platform gates.

View File

@@ -0,0 +1,184 @@
using Age.Engine.Hosting;
using Age.Engine.Model;
namespace Age.Engine.Vm;
public sealed partial class VirtualMachine
{
private int StepInput(string label, IReadOnlyList<Operand> a, int pc)
{
switch (label)
{
case "wait-for-input":
RefreshAdvReadSkipState();
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
if (_o.HaltAtWaitForInput) { HaltReason ??= "wait-for-input"; return HALT; }
// The native ADV chrome is a coroutine: after an earlier 0x93 cancellation its shared
// registration pass runs again before a stable message wait. Our blocking host models that
// scheduler boundary by re-arming the frame's retained definitions here.
bool wakeAtWait = false;
lock (_interactiveLock)
{
if (_cur.Hotspots.HasDefinitions && !_cur.Hotspots.Armed)
{
_interactiveFrame = _cur;
wakeAtWait = _cur.Hotspots.Arm(_pointerX, _pointerY);
}
}
if (wakeAtWait) _host.WakeInputCallbackService();
_host.WaitForInput((int)Read(a[0]), ServiceHotspotCallback,
() => new AdvAutoWaitState(_autoMessageEnabled, _autoVoicePending,
_autoMessageTime0Ms, _autoMessageTime1Ms));
_sharedProfile.ReadText.QueueMessage(
_cur.Script.PackedId, CurrentReadMessageIndex(),
_cur.Script.ReadMessageOffsets.Count);
return pc + 1;
case "u0041BEB0":
case "register-hotspot-callbacks": // 0x90: inclusive rect + enter/leave/activate local callbacks
lock (_interactiveLock)
_cur.Hotspots.Register((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]));
return pc + 1;
case "u00415040":
case "cancel-hotspot-wait": // 0x93
lock (_interactiveLock)
{
_cur.Hotspots.Reset();
if (ReferenceEquals(_interactiveFrame, _cur)) _interactiveFrame = null;
}
return pc + 1;
case "u00415090":
case "arm-hotspot-wait": // 0x94
{
bool wake;
lock (_interactiveLock)
{
_interactiveFrame = _cur;
wake = _cur.Hotspots.Arm(_pointerX, _pointerY);
}
if (wake) _host.WakeInputCallbackService();
return pc + 1;
}
case "u0041C150":
case "bind-hotspot-key": // 0x97: bind a configured logical action to this record
lock (_interactiveLock)
_cur.Hotspots.BindKey((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]),
(int)Read(a[3]), (int)Read(a[4]));
return pc + 1;
case "u0041B210":
case "set-cursor-resource": // 0x86: raw indexed .CUR resource
_host.SetCursorResource(Read(a[0])); return pc + 1;
case "u00414D10":
case "clear-cursor-resource": // 0x87
_host.ClearCursorResource(); return pc + 1;
case "mouse_callback":
case "register-mouse-callback": // 0xcc (poll interval ms, local target dword offset)
_cur.MouseCallbackIntervalMs = System.Math.Max(0, Read(a[0]));
_cur.MouseCallbackTarget = (int)Read(a[1]);
_cur.MouseCallbackNextAtMs = _host.InputClockMilliseconds + _cur.MouseCallbackIntervalMs;
lock (_interactiveLock) _rawInputFrame = _cur;
return pc + 1;
case "get-input-type":
case "dispatch-mouse-callback": // 0xcd
{
long now = _host.InputClockMilliseconds;
if (_cur.MouseCallbackTarget < 0 || now < _cur.MouseCallbackNextAtMs) return pc + 1;
_cur.MouseCallbackNextAtMs = now + _cur.MouseCallbackIntervalMs;
if (!_cur.Script.IndexByOffset.TryGetValue(_cur.MouseCallbackTarget, out int target))
return pc + 1;
_cur.CallStack.Add(pc + 1);
return target;
}
case "joy_callback":
case "register-joy-callback": // 0xfb (input index, local target dword offset)
{
int index = (int)Read(a[0]);
if ((uint)index < 32) _cur.InputCallbackTargets[index] = (int)Read(a[1]);
return pc + 1;
}
case "u0041E360":
case "set-input-action-count": // 0xfe: actions [0,count), no-input callback at count
{
int count = unchecked((int)Read(a[0]));
if (!InputBindings.SetActionCount(count))
{
HaltReason ??= $"input-action-count-out-of-range:{count}";
return HALT;
}
return pc + 1;
}
case "u00415A10":
case "poll-joy-callback-input": // 0xff
_cur.PendingInputCallbackMask = InputBindings.PollActionMask()
| Volatile.Read(ref _heldInputCallbackMask)
| Interlocked.Exchange(ref _queuedInputCallbackMask, 0);
_cur.InputCallbackScanIndex = 0;
return pc + 1;
case "u00415A60":
case "dispatch-joy-callbacks": // 0x100
{
int actionCount = InputBindings.ActionCount;
if (_cur.PendingInputCallbackMask == 0)
{
int idleTarget = _cur.InputCallbackTargets[actionCount];
if (idleTarget < 0 || !_cur.Script.IndexByOffset.TryGetValue(idleTarget, out int target))
return pc + 1;
_cur.CallStack.Add(pc + 1);
return target;
}
while (_cur.InputCallbackScanIndex < actionCount)
{
int index = _cur.InputCallbackScanIndex++;
if ((_cur.PendingInputCallbackMask & (1 << index)) == 0) continue;
int targetOffset = _cur.InputCallbackTargets[index];
if (targetOffset < 0 || !_cur.Script.IndexByOffset.TryGetValue(targetOffset, out int target))
continue;
// Resume on op 0x100 so another simultaneously active input can dispatch.
_cur.CallStack.Add(pc);
return target;
}
return pc + 1;
}
case "u0041E500":
case "map-joystick-button": // 0x107: button slot N emits action N+4
InputBindings.MapJoystickButton(unchecked((int)Read(a[0])), unchecked((int)Read(a[1])));
return pc + 1;
case "u00415E70":
case "get-mouse-button-state": // 0x108
Write(a[0], Volatile.Read(ref _mouseButtonState)); return pc + 1;
case "u00415F10":
case "consume-mouse-wheel-delta": // 0x10d
Write(a[0], Interlocked.Exchange(ref _mouseWheelDelta, 0)); return pc + 1;
case "u00415EC0":
case "get-cursor-virtual": // 0x109
{
int x, y;
lock (_interactiveLock) { x = _pointerX; y = _pointerY; }
Write(a[0], x == int.MinValue ? 0 : x);
Write(a[1], y == int.MinValue ? 0 : y);
return pc + 1;
}
case "u0041E540":
case "set-cursor-virtual": // 0x10a; retain the virtual position even without OS warping
UpdatePointer((int)Read(a[0]), (int)Read(a[1])); return pc + 1;
case "u0041E5A0":
case "map-mouse-button": // 0x10b: physical button -> slot, polled action is slot+4
InputBindings.MapMouseButton(unchecked((int)Read(a[0])), unchecked((int)Read(a[1])));
return pc + 1;
case "u0041E5E0":
case "map-keyboard-scancode": // 0x10c: logical action <- DIK translated through native VK table
{
int action = unchecked((int)Read(a[0]));
int dik = unchecked((int)Read(a[1]));
if (!InputBindings.MapKeyboardScanCode(action, dik))
{
HaltReason ??= $"keyboard-action-out-of-range:{action}";
return HALT;
}
return pc + 1;
}
default:
throw new InvalidOperationException($"Non-input opcode routed to input handler: {label}");
}
}
}

View File

@@ -1978,173 +1978,47 @@ public sealed partial class VirtualMachine
case "draw-formatted-integer": // 0x205 (surface slot, x, y, value, field width, flags) case "draw-formatted-integer": // 0x205 (surface slot, x, y, value, field width, flags)
return StepAdvText(label, ins, pc); return StepAdvText(label, ins, pc);
case "wait-for-input": case "wait-for-input":
RefreshAdvReadSkipState();
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
if (_o.HaltAtWaitForInput) { HaltReason ??= "wait-for-input"; return HALT; }
// The native ADV chrome is a coroutine: after an earlier 0x93 cancellation its shared
// registration pass runs again before a stable message wait. Our blocking host models that
// scheduler boundary by re-arming the frame's retained definitions here.
bool wakeAtWait = false;
lock (_interactiveLock)
{
if (_cur.Hotspots.HasDefinitions && !_cur.Hotspots.Armed)
{
_interactiveFrame = _cur;
wakeAtWait = _cur.Hotspots.Arm(_pointerX, _pointerY);
}
}
if (wakeAtWait) _host.WakeInputCallbackService();
_host.WaitForInput((int)Read(a[0]), ServiceHotspotCallback,
() => new AdvAutoWaitState(_autoMessageEnabled, _autoVoicePending,
_autoMessageTime0Ms, _autoMessageTime1Ms));
_sharedProfile.ReadText.QueueMessage(
_cur.Script.PackedId, CurrentReadMessageIndex(),
_cur.Script.ReadMessageOffsets.Count);
return pc + 1;
case "u0041BEB0": case "u0041BEB0":
case "register-hotspot-callbacks": // 0x90: inclusive rect + enter/leave/activate local callbacks case "register-hotspot-callbacks": // 0x90: inclusive rect + enter/leave/activate local callbacks
lock (_interactiveLock)
_cur.Hotspots.Register((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]));
return pc + 1;
case "u00415040": case "u00415040":
case "cancel-hotspot-wait": // 0x93 case "cancel-hotspot-wait": // 0x93
lock (_interactiveLock)
{
_cur.Hotspots.Reset();
if (ReferenceEquals(_interactiveFrame, _cur)) _interactiveFrame = null;
}
return pc + 1;
case "u00415090": case "u00415090":
case "arm-hotspot-wait": // 0x94 case "arm-hotspot-wait": // 0x94
{
bool wake;
lock (_interactiveLock)
{
_interactiveFrame = _cur;
wake = _cur.Hotspots.Arm(_pointerX, _pointerY);
}
if (wake) _host.WakeInputCallbackService();
return pc + 1;
}
case "u0041C150": case "u0041C150":
case "bind-hotspot-key": // 0x97: bind a configured logical action to this record case "bind-hotspot-key": // 0x97: bind a configured logical action to this record
lock (_interactiveLock)
_cur.Hotspots.BindKey((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]),
(int)Read(a[3]), (int)Read(a[4]));
return pc + 1;
case "u0041B210": case "u0041B210":
case "set-cursor-resource": // 0x86: raw indexed .CUR resource case "set-cursor-resource": // 0x86: raw indexed .CUR resource
_host.SetCursorResource(Read(a[0])); return pc + 1;
case "u00414D10": case "u00414D10":
case "clear-cursor-resource": // 0x87 case "clear-cursor-resource": // 0x87
_host.ClearCursorResource(); return pc + 1; return StepInput(label, a, pc);
case "mouse_callback": case "mouse_callback":
case "register-mouse-callback": // 0xcc (poll interval ms, local target dword offset) case "register-mouse-callback": // 0xcc (poll interval ms, local target dword offset)
_cur.MouseCallbackIntervalMs = System.Math.Max(0, Read(a[0]));
_cur.MouseCallbackTarget = (int)Read(a[1]);
_cur.MouseCallbackNextAtMs = _host.InputClockMilliseconds + _cur.MouseCallbackIntervalMs;
lock (_interactiveLock) _rawInputFrame = _cur;
return pc + 1;
case "get-input-type": case "get-input-type":
case "dispatch-mouse-callback": // 0xcd case "dispatch-mouse-callback": // 0xcd
{
long now = _host.InputClockMilliseconds;
if (_cur.MouseCallbackTarget < 0 || now < _cur.MouseCallbackNextAtMs) return pc + 1;
_cur.MouseCallbackNextAtMs = now + _cur.MouseCallbackIntervalMs;
if (!_cur.Script.IndexByOffset.TryGetValue(_cur.MouseCallbackTarget, out int target))
return pc + 1;
_cur.CallStack.Add(pc + 1);
return target;
}
case "joy_callback": case "joy_callback":
case "register-joy-callback": // 0xfb (input index, local target dword offset) case "register-joy-callback": // 0xfb (input index, local target dword offset)
{
int index = (int)Read(a[0]);
if ((uint)index < 32) _cur.InputCallbackTargets[index] = (int)Read(a[1]);
return pc + 1;
}
case "u0041E360": case "u0041E360":
case "set-input-action-count": // 0xfe: actions [0,count), no-input callback at count case "set-input-action-count": // 0xfe: actions [0,count), no-input callback at count
{
int count = unchecked((int)Read(a[0]));
if (!InputBindings.SetActionCount(count))
{
HaltReason ??= $"input-action-count-out-of-range:{count}";
return HALT;
}
return pc + 1;
}
case "u00415A10": case "u00415A10":
case "poll-joy-callback-input": // 0xff case "poll-joy-callback-input": // 0xff
_cur.PendingInputCallbackMask = InputBindings.PollActionMask()
| Volatile.Read(ref _heldInputCallbackMask)
| Interlocked.Exchange(ref _queuedInputCallbackMask, 0);
_cur.InputCallbackScanIndex = 0;
return pc + 1;
case "u00415A60": case "u00415A60":
case "dispatch-joy-callbacks": // 0x100 case "dispatch-joy-callbacks": // 0x100
{ return StepInput(label, a, pc);
int actionCount = InputBindings.ActionCount;
if (_cur.PendingInputCallbackMask == 0)
{
int idleTarget = _cur.InputCallbackTargets[actionCount];
if (idleTarget < 0 || !_cur.Script.IndexByOffset.TryGetValue(idleTarget, out int target))
return pc + 1;
_cur.CallStack.Add(pc + 1);
return target;
}
while (_cur.InputCallbackScanIndex < actionCount)
{
int index = _cur.InputCallbackScanIndex++;
if ((_cur.PendingInputCallbackMask & (1 << index)) == 0) continue;
int targetOffset = _cur.InputCallbackTargets[index];
if (targetOffset < 0 || !_cur.Script.IndexByOffset.TryGetValue(targetOffset, out int target))
continue;
// Resume on op 0x100 so another simultaneously active input can dispatch.
_cur.CallStack.Add(pc);
return target;
}
return pc + 1;
}
case "u0041E500": case "u0041E500":
case "map-joystick-button": // 0x107: button slot N emits action N+4 case "map-joystick-button": // 0x107: button slot N emits action N+4
InputBindings.MapJoystickButton(unchecked((int)Read(a[0])), unchecked((int)Read(a[1])));
return pc + 1;
case "u00415E70": case "u00415E70":
case "get-mouse-button-state": // 0x108 case "get-mouse-button-state": // 0x108
Write(a[0], Volatile.Read(ref _mouseButtonState)); return pc + 1;
case "u00415F10": case "u00415F10":
case "consume-mouse-wheel-delta": // 0x10d case "consume-mouse-wheel-delta": // 0x10d
Write(a[0], Interlocked.Exchange(ref _mouseWheelDelta, 0)); return pc + 1;
case "u00415EC0": case "u00415EC0":
case "get-cursor-virtual": // 0x109 case "get-cursor-virtual": // 0x109
{
int x, y;
lock (_interactiveLock) { x = _pointerX; y = _pointerY; }
Write(a[0], x == int.MinValue ? 0 : x);
Write(a[1], y == int.MinValue ? 0 : y);
return pc + 1;
}
case "u0041E540": case "u0041E540":
case "set-cursor-virtual": // 0x10a; retain the virtual position even without OS warping case "set-cursor-virtual": // 0x10a; retain the virtual position even without OS warping
UpdatePointer((int)Read(a[0]), (int)Read(a[1])); return pc + 1;
case "u0041E5A0": case "u0041E5A0":
case "map-mouse-button": // 0x10b: physical button -> slot, polled action is slot+4 case "map-mouse-button": // 0x10b: physical button -> slot, polled action is slot+4
InputBindings.MapMouseButton(unchecked((int)Read(a[0])), unchecked((int)Read(a[1])));
return pc + 1;
case "u0041E5E0": case "u0041E5E0":
case "map-keyboard-scancode": // 0x10c: logical action <- DIK translated through native VK table case "map-keyboard-scancode": // 0x10c: logical action <- DIK translated through native VK table
{ return StepInput(label, a, pc);
int action = unchecked((int)Read(a[0]));
int dik = unchecked((int)Read(a[1]));
if (!InputBindings.MapKeyboardScanCode(action, dik))
{
HaltReason ??= $"keyboard-action-out-of-range:{action}";
return HALT;
}
return pc + 1;
}
case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing. case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing.
_host.Sleep(Read(a[0])); return pc + 1; _host.Sleep(Read(a[0])); return pc + 1;
case "u00425960": case "u00425960":