Extract VM input opcode handler
This commit is contained in:
@@ -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
|
||||
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.
|
||||
`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
|
||||
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports
|
||||
|
||||
@@ -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
|
||||
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
|
||||
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
|
||||
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`
|
||||
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
|
||||
now isolated as well, extract the interactive input/hotspot/cursor callback opcode family next without replacing
|
||||
the proven dispatcher or changing public types, commands, and generated output.
|
||||
and interactive input/hotspot/cursor callbacks now isolated as well, extract the sleep/timed-callback sequencing
|
||||
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
|
||||
not replace Phase B gameplay validation or the open cross-platform gates.
|
||||
|
||||
184
engine/Age.Engine/Vm/VirtualMachine.Input.cs
Normal file
184
engine/Age.Engine/Vm/VirtualMachine.Input.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1978,173 +1978,47 @@ public sealed partial class VirtualMachine
|
||||
case "draw-formatted-integer": // 0x205 (surface slot, x, y, value, field width, flags)
|
||||
return StepAdvText(label, ins, pc);
|
||||
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;
|
||||
return StepInput(label, a, pc);
|
||||
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;
|
||||
}
|
||||
return StepInput(label, a, pc);
|
||||
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;
|
||||
}
|
||||
return StepInput(label, a, pc);
|
||||
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 "u00425960":
|
||||
|
||||
Reference in New Issue
Block a user