Implement script-configured input bindings

This commit is contained in:
gamer147
2026-07-21 10:32:37 -04:00
parent 490243705b
commit be586ca988
16 changed files with 679 additions and 75 deletions

View File

@@ -16,9 +16,13 @@ public class HistoryInteractionOpsTests
private sealed class StopAfterHistoryVoiceException : Exception { }
private sealed class StopAfterHistoryWheelException : Exception { }
private static void SeedSystem4AdvLayouts(Sys4ScriptProvider scripts, AdvTextHistory history)
=> Assert.Equal(9, AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets(
scripts.RequireByName("SYSTEM4.BIN"), Table, history));
private static void SeedSystem4Services(Sys4ScriptProvider scripts, VirtualMachine vm)
{
var systemScript = scripts.RequireByName("SYSTEM4.BIN");
Assert.Equal(9, AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets(
systemScript, Table, vm.TextHistory));
Assert.Equal(16, InputBindingBootstrap.Apply(systemScript, vm.InputBindings));
}
private sealed class Sc0000HistoryCloseHost : RecordingHost
{
@@ -182,7 +186,7 @@ public class HistoryInteractionOpsTests
var vm = new VirtualMachine(scripts.RequireByName("SC0000.BIN"), Table, host,
new VmOptions(MaxSteps: 2_000_000), scripts);
host.Vm = vm;
SeedSystem4AdvLayouts(scripts, vm.TextHistory);
SeedSystem4Services(scripts, vm);
vm.Globals[0x6c1] = 1;
Assert.Throws<StopAfterHistoryWheelException>(() => vm.Run());
@@ -199,7 +203,7 @@ public class HistoryInteractionOpsTests
var vm = new VirtualMachine(scripts.RequireByName("SC0000.BIN"), Table, host,
new VmOptions(MaxSteps: 2_000_000), scripts);
host.Vm = vm;
SeedSystem4AdvLayouts(scripts, vm.TextHistory);
SeedSystem4Services(scripts, vm);
vm.Globals[0x6c1] = 1;
Assert.Throws<StopAfterHistoryReturnsException>(() => vm.Run());
@@ -285,7 +289,7 @@ public class HistoryInteractionOpsTests
var vm = new VirtualMachine(scripts.RequireByName("SC0000.BIN"), Table, host,
new VmOptions(MaxSteps: 2_000_000), scripts);
host.Vm = vm;
SeedSystem4AdvLayouts(scripts, vm.TextHistory);
SeedSystem4Services(scripts, vm);
vm.Globals[0x6c1] = 1;
Assert.Throws<StopAfterHistoryReturnsException>(() => vm.Run());
@@ -326,7 +330,7 @@ public class HistoryInteractionOpsTests
var vm = new VirtualMachine(scripts.RequireByName("SC0000.BIN"), Table, host,
new VmOptions(MaxSteps: 2_000_000), scripts);
host.Vm = vm;
SeedSystem4AdvLayouts(scripts, vm.TextHistory);
SeedSystem4Services(scripts, vm);
vm.Globals[0x6c1] = 1;
Assert.Throws<StopAfterHistoryVoiceException>(() => vm.Run());

View File

@@ -395,6 +395,8 @@ public class HotspotInputTests
var host = new Sc0000HideWindowHost();
var vm = new VirtualMachine(scene, table, host, new VmOptions(MaxSteps: 1_000_000), provider, trace);
host.Vm = vm;
InputBindingBootstrap.Apply(
Sys4Loader.Load(Paths.Scripts()["SYSTEM4.BIN"], table), vm.InputBindings);
vm.Globals[0x6c1] = 1;
vm.Globals[0x62425] = 1; // inherited native ADV scheduler state, mirrored by Godot Main

View File

@@ -0,0 +1,135 @@
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
public class InputBindingTests
{
private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson);
private static Operand I(long value) => new(0, value);
private static Operand G(long address) => new(3, address);
[Fact]
public void NativeDefaultsExposeSevenKeyboardActionsAndMouseActionFour()
{
var bindings = new InputBindings();
Assert.Equal(7, bindings.ActionCount);
Assert.Equal(0, bindings.KeyboardAction(0x26)); // Up
Assert.Equal(1, bindings.KeyboardAction(0x27)); // Right
Assert.Equal(2, bindings.KeyboardAction(0x28)); // Down
Assert.Equal(3, bindings.KeyboardAction(0x25)); // Left
Assert.Equal(4, bindings.KeyboardAction(0x0d)); // Enter
Assert.Equal(5, bindings.KeyboardAction(0x20)); // Space
Assert.Equal(6, bindings.KeyboardAction(0x08)); // Backspace
Assert.Equal(4, bindings.MouseAction(0));
Assert.Equal(4, bindings.MouseAction(1));
}
[Fact]
public void ConfigurationOpcodesMutateTheSharedPhysicalBindingService()
{
var script = ScriptAssembler.Assemble(Table, "INPUT_CONFIG", new List<(int, Operand[])>
{
(0xfe, new[] { I(10) }),
(0x107, new[] { I(0), I(3) }),
(0x10b, new[] { I(3), I(1) }),
(0x10c, new[] { I(4), I(0x2c) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var vm = new VirtualMachine(script, Table, new RecordingHost());
vm.Run();
Assert.Equal(10, vm.InputBindings.ActionCount);
Assert.Equal(4, vm.InputBindings.KeyboardAction(0x5a));
Assert.Equal(7, vm.InputBindings.MouseAction(1));
Assert.NotEqual(0, vm.InputBindings.JoystickButtonActionMask(3) & (1 << 4));
}
[Fact]
public void System4BootstrapReplaysAllSixteenInputConfigurationCalls()
{
var scripts = Sys4ScriptProvider.Load(Table);
var bindings = new InputBindings();
Assert.Equal(16, InputBindingBootstrap.Apply(
scripts.RequireByName("SYSTEM4.BIN"), bindings));
Assert.Equal(10, bindings.ActionCount);
Assert.Equal(4, bindings.KeyboardAction(0x5a)); // Z plus retained Enter
Assert.Equal(4, bindings.KeyboardAction(0x0d));
Assert.Equal(5, bindings.KeyboardAction(0x20));
Assert.Equal(6, bindings.KeyboardAction(0x08)); // retained Backspace
Assert.Equal(6, bindings.KeyboardAction(0x11)); // Ctrl
Assert.Equal(7, bindings.KeyboardAction(0x58)); // X
Assert.Equal(8, bindings.KeyboardAction(0x21)); // PageUp
Assert.Equal(9, bindings.KeyboardAction(0x22)); // PageDown
Assert.Equal(7, bindings.MouseAction(1));
}
[Fact]
public void EmptyPollDispatchesTheCallbackAtActionCount()
{
var script = ScriptAssembler.Assemble(Table, "INPUT_IDLE", new List<(int, Operand[])>
{
(0xfe, new[] { I(2) }), // offsets 0..2
(0xfb, new[] { I(2), I(11) }), // offsets 3..7
(0xff, Array.Empty<Operand>()), // offset 8
(0x100, Array.Empty<Operand>()), // offset 9
(0x2, Array.Empty<Operand>()), // offset 10
(0x55, new[] { G(0x700), I(1) }), // offset 11
(0x5, Array.Empty<Operand>()),
}, Array.Empty<string>());
var vm = new VirtualMachine(script, Table, new RecordingHost());
vm.Run();
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x700));
}
[Fact]
public void HeldActionsDispatchSimultaneouslyButOnlyBelowConfiguredCount()
{
var script = ScriptAssembler.Assemble(Table, "INPUT_MULTI", new List<(int, Operand[])>
{
(0xfe, new[] { I(2) }), // offsets 0..2
(0xfb, new[] { I(0), I(16) }), // offsets 3..7
(0xfb, new[] { I(1), I(22) }), // offsets 8..12
(0xff, Array.Empty<Operand>()), // offset 13
(0x100, Array.Empty<Operand>()), // offset 14
(0x2, Array.Empty<Operand>()), // offset 15
(0x55, new[] { G(0x701), I(1) }), // offset 16
(0x5, Array.Empty<Operand>()), // offset 21
(0x55, new[] { G(0x702), I(1) }), // offset 22
(0x5, Array.Empty<Operand>()),
}, Array.Empty<string>());
var vm = new VirtualMachine(script, Table, new RecordingHost());
vm.UpdateInputCallbackState(0, true);
vm.UpdateInputCallbackState(1, true);
vm.UpdateInputCallbackState(3, true); // bit survives polling but is outside count and is ignored
vm.Run();
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x701));
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x702));
}
[Theory]
[InlineData(0xfe, "input-action-count-out-of-range:32")]
[InlineData(0x10c, "keyboard-action-out-of-range:32")]
public void NativeValidatedActionOperandsRejectThirtyTwo(int opcode, string expected)
{
Operand[] args = opcode == 0xfe ? new[] { I(32) } : new[] { I(32), I(0x2c) };
var script = ScriptAssembler.Assemble(Table, "INPUT_RANGE", new List<(int, Operand[])>
{
(opcode, args),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var vm = new VirtualMachine(script, Table, new RecordingHost());
vm.Run();
Assert.Equal(expected, vm.HaltReason);
}
}

View File

@@ -0,0 +1,199 @@
namespace Age.Engine.Model;
/// <summary>
/// AGE's process-owned physical-to-logical input map. Scripts configure the logical action count and
/// keyboard/mouse/joystick bindings; op 0xff polls this state and op 0x100 dispatches the resulting mask.
/// Win32 virtual-key values are retained as the keyboard ABI because native op 0x10c translates DIK scan
/// codes through that table before installing a mapping.
/// </summary>
public sealed class InputBindings
{
public const int MaximumActions = 32;
public const int DefaultActionCount = 7;
public const double JoystickAxisThreshold = 0.5;
private readonly object _lock = new();
private readonly int[] _keyboardActions = Enumerable.Repeat(-1, 256).ToArray();
private readonly int[] _mouseButtonSlots = new int[2];
private readonly int[] _joystickButtons = new int[MaximumActions];
private readonly HashSet<int> _heldVirtualKeys = new();
private uint _heldMouseButtons;
private uint _heldJoystickButtons;
private double _joystickX;
private double _joystickY;
private int _actionCount = DefaultActionCount;
public InputBindings()
{
// input_manager_initialize_defaults@0x460630: seven default actions before SYSTEM4 extends
// the table to ten. Several physical keys may map to the same logical action.
MapKeyboardScanCode(0, 0xc8); // Up
MapKeyboardScanCode(1, 0xcd); // Right
MapKeyboardScanCode(2, 0xd0); // Down
MapKeyboardScanCode(3, 0xcb); // Left
MapKeyboardScanCode(4, 0x1c); // Enter
MapKeyboardScanCode(5, 0x39); // Space
MapKeyboardScanCode(6, 0x0e); // Backspace
}
public int ActionCount
{
get { lock (_lock) return _actionCount; }
}
public bool SetActionCount(int count)
{
if ((uint)count >= MaximumActions) return false;
lock (_lock) _actionCount = count;
return true;
}
public bool MapKeyboardScanCode(int action, int dikScanCode)
{
if ((uint)action >= MaximumActions) return false;
int virtualKey = DikToVirtualKey(dikScanCode);
if (virtualKey == 0) return true; // Native writes VK[0]; GetAsyncKeyState(0) never contributes.
lock (_lock) _keyboardActions[virtualKey] = action;
return true;
}
public void MapJoystickButton(int buttonSlot, int physicalButton)
{
if ((uint)buttonSlot >= MaximumActions) return;
lock (_lock) _joystickButtons[buttonSlot] = physicalButton;
}
public void MapMouseButton(int buttonSlot, int physicalButton)
{
if ((uint)buttonSlot >= MaximumActions || (uint)physicalButton >= _mouseButtonSlots.Length) return;
lock (_lock) _mouseButtonSlots[physicalButton] = buttonSlot;
}
public int KeyboardAction(int virtualKey)
{
if ((uint)virtualKey >= _keyboardActions.Length) return -1;
lock (_lock) return _keyboardActions[virtualKey];
}
public int MouseAction(int physicalButton)
{
if ((uint)physicalButton >= _mouseButtonSlots.Length) return -1;
lock (_lock) return (_mouseButtonSlots[physicalButton] + 4) & 31;
}
public int JoystickButtonActionMask(int physicalButton)
{
if ((uint)physicalButton >= 32) return 0;
lock (_lock) return JoystickButtonActionMaskLocked(physicalButton);
}
public void UpdateKeyboardVirtualKey(int virtualKey, bool pressed)
{
if ((uint)virtualKey >= _keyboardActions.Length) return;
lock (_lock)
{
if (pressed) _heldVirtualKeys.Add(virtualKey);
else _heldVirtualKeys.Remove(virtualKey);
}
}
public void UpdateMouseButton(int physicalButton, bool pressed)
{
if ((uint)physicalButton >= 32) return;
lock (_lock) UpdateBit(ref _heldMouseButtons, physicalButton, pressed);
}
public void UpdateJoystickButton(int physicalButton, bool pressed)
{
if ((uint)physicalButton >= 32) return;
lock (_lock) UpdateBit(ref _heldJoystickButtons, physicalButton, pressed);
}
public void UpdateJoystickAxes(double x, double y)
{
lock (_lock)
{
_joystickX = Math.Clamp(x, -1.0, 1.0);
_joystickY = Math.Clamp(y, -1.0, 1.0);
}
}
public void UpdateJoystickAxis(int axis, double value)
{
lock (_lock)
{
if (axis == 0) _joystickX = Math.Clamp(value, -1.0, 1.0);
else if (axis == 1) _joystickY = Math.Clamp(value, -1.0, 1.0);
}
}
public int PollActionMask()
{
lock (_lock)
{
int mask = 0;
foreach (int virtualKey in _heldVirtualKeys)
{
int action = _keyboardActions[virtualKey];
if ((uint)action < MaximumActions) mask |= 1 << action;
}
// Native polls only VK_LBUTTON/VK_RBUTTON. Their zero-initialized slots both produce
// action 4 until scripts remap one of them.
for (int button = 0; button < _mouseButtonSlots.Length; button++)
if ((_heldMouseButtons & (1u << button)) != 0)
mask |= 1 << ((_mouseButtonSlots[button] + 4) & 31);
if (_joystickY < -JoystickAxisThreshold) mask |= 1 << 0;
else if (_joystickY > JoystickAxisThreshold) mask |= 1 << 2;
if (_joystickX > JoystickAxisThreshold) mask |= 1 << 1;
else if (_joystickX < -JoystickAxisThreshold) mask |= 1 << 3;
for (int physicalButton = 0; physicalButton < 32; physicalButton++)
if ((_heldJoystickButtons & (1u << physicalButton)) != 0)
mask |= JoystickButtonActionMaskLocked(physicalButton);
return mask;
}
}
private int JoystickButtonActionMaskLocked(int physicalButton)
{
int mask = 0;
// Native loops input_action_count slots and uses x86's masked shift for slot+4.
for (int slot = 0; slot < _actionCount; slot++)
if (_joystickButtons[slot] == physicalButton) mask |= 1 << ((slot + 4) & 31);
return mask;
}
private static void UpdateBit(ref uint field, int bit, bool set)
{
uint mask = 1u << bit;
field = set ? field | mask : field & ~mask;
}
/// <summary>The DIK-to-VK entries populated by input_initialize_dik_to_vk_table@0x45fc60.</summary>
public static int DikToVirtualKey(int dik) => dik switch
{
0x01 => 0x1b, 0x02 => 0x31, 0x03 => 0x32, 0x04 => 0x33, 0x05 => 0x34,
0x06 => 0x35, 0x07 => 0x36, 0x08 => 0x37, 0x09 => 0x38, 0x0a => 0x39,
0x0b => 0x30, 0x0c => 0x6d, 0x0e => 0x08, 0x0f => 0x09,
0x10 => 0x51, 0x11 => 0x57, 0x12 => 0x45, 0x13 => 0x52, 0x14 => 0x54,
0x15 => 0x59, 0x16 => 0x55, 0x17 => 0x49, 0x18 => 0x4f, 0x19 => 0x50,
0x1c => 0x0d, 0x1d => 0x11, 0x1e => 0x41, 0x1f => 0x53, 0x20 => 0x44,
0x21 => 0x46, 0x22 => 0x47, 0x23 => 0x48, 0x24 => 0x4a, 0x25 => 0x4b,
0x26 => 0x4c, 0x2a => 0x10, 0x2c => 0x5a, 0x2d => 0x58, 0x2e => 0x43,
0x2f => 0x56, 0x30 => 0x42, 0x31 => 0x4e, 0x32 => 0x4d, 0x36 => 0x10,
0x38 => 0x12, 0x39 => 0x20,
0x3b => 0x70, 0x3c => 0x71, 0x3d => 0x72, 0x3e => 0x73, 0x3f => 0x74,
0x40 => 0x75, 0x41 => 0x76, 0x42 => 0x77, 0x43 => 0x78, 0x44 => 0x79,
0x45 => 0x90, 0x46 => 0x91, 0x47 => 0x67, 0x48 => 0x68, 0x49 => 0x69,
0x4a => 0x6d, 0x4b => 0x64, 0x4c => 0x65, 0x4d => 0x66, 0x4e => 0x6b,
0x4f => 0x61, 0x50 => 0x62, 0x51 => 0x63, 0x52 => 0x60, 0x53 => 0x6e,
0x57 => 0x7a, 0x58 => 0x7b, 0x70 => 0x15, 0x79 => 0x1c, 0x7b => 0x1d,
0x94 => 0x19, 0x9c => 0x0d, 0x9d => 0x11, 0xb5 => 0x6f, 0xb8 => 0x12,
0xc7 => 0x12, 0xc8 => 0x26, 0xc9 => 0x21, 0xcb => 0x25,
0xcd => 0x27, 0xcf => 0x23, 0xd0 => 0x28, 0xd1 => 0x22, 0xd2 => 0x2d,
0xdb => 0x5b, 0xdc => 0x5c, 0xdd => 0x5d,
_ => 0,
};
}

View File

@@ -0,0 +1,38 @@
using Age.Engine.Model;
namespace Age.Engine.Vm;
/// <summary>
/// Replays SYSTEM4's data-only input configuration for a direct-scene diagnostic harness which starts
/// after the persistent system root. Natural boot executes the same opcode handlers normally.
/// </summary>
public static class InputBindingBootstrap
{
public static int Apply(Script systemScript, InputBindings bindings)
{
ArgumentNullException.ThrowIfNull(systemScript);
ArgumentNullException.ThrowIfNull(bindings);
int applied = 0;
foreach (Instruction instruction in systemScript.Instructions)
{
if (instruction.Args.Any(arg => arg.Type != 0)) continue;
long A(int index) => instruction.Args[index].Value;
switch (instruction.Opcode)
{
case 0xfe when instruction.Args.Count == 1:
if (bindings.SetActionCount((int)A(0))) applied++;
break;
case 0x107 when instruction.Args.Count == 2:
bindings.MapJoystickButton((int)A(0), (int)A(1)); applied++;
break;
case 0x10b when instruction.Args.Count == 2:
bindings.MapMouseButton((int)A(0), (int)A(1)); applied++;
break;
case 0x10c when instruction.Args.Count == 2:
if (bindings.MapKeyboardScanCode((int)A(0), (int)A(1))) applied++;
break;
}
}
return applied;
}
}

View File

@@ -57,6 +57,7 @@ public sealed class VirtualMachine
public Dictionary<int, long> ExternalGlobals { get; } = new();
public Dictionary<int, string> GlobalStrings { get; } = new();
public GfxState Gfx { get; } = new();
public InputBindings InputBindings { get; } = new();
public List<(int Offset, string Text, string Script)> Emitted { get; } = new();
public string? HaltReason { get; private set; }
public long Steps { get; private set; }
@@ -144,17 +145,52 @@ public sealed class VirtualMachine
_host.WakeInputCallbackService();
}
/// <summary>Update one held AGE input-callback index used by ops 0xfb/0xff/0x100.</summary>
/// <summary>Update one held logical AGE action directly. Physical frontends should use the
/// keyboard/mouse/joystick methods below so script-configured bindings remain authoritative.</summary>
public void UpdateInputCallbackState(int index, bool pressed)
{
if ((uint)index >= 32) return;
UpdateMaskBit(ref _heldInputCallbackMask, 1 << index, pressed);
_host.WakeInputCallbackService();
}
/// <summary>Queue a one-shot AGE input callback, such as the shared release callback at index 10.</summary>
public int UpdateKeyboardVirtualKeyState(int virtualKey, bool pressed)
{
InputBindings.UpdateKeyboardVirtualKey(virtualKey, pressed);
_host.WakeInputCallbackService();
return InputBindings.KeyboardAction(virtualKey);
}
public int UpdatePhysicalMouseButtonState(int physicalButton, bool pressed)
{
InputBindings.UpdateMouseButton(physicalButton, pressed);
_host.WakeInputCallbackService();
return InputBindings.MouseAction(physicalButton);
}
public int UpdateJoystickButtonState(int physicalButton, bool pressed)
{
InputBindings.UpdateJoystickButton(physicalButton, pressed);
_host.WakeInputCallbackService();
return InputBindings.JoystickButtonActionMask(physicalButton);
}
public void UpdateJoystickAxisState(int axis, double value)
{
InputBindings.UpdateJoystickAxis(axis, value);
_host.WakeInputCallbackService();
}
/// <summary>Queue a one-shot logical action for diagnostics/tests. The native no-input callback is
/// table index ActionCount and is selected automatically when polling returns an empty mask.</summary>
public void QueueInputCallback(int index)
{
if ((uint)index >= 32) return;
if (index == InputBindings.ActionCount)
{
_host.WakeInputCallbackService();
return;
}
int bit = 1 << index;
int before, after;
do
@@ -162,6 +198,7 @@ public sealed class VirtualMachine
before = Volatile.Read(ref _queuedInputCallbackMask);
after = before | bit;
} while (Interlocked.CompareExchange(ref _queuedInputCallbackMask, after, before) != before);
_host.WakeInputCallbackService();
}
private static void UpdateMaskBit(ref int field, int bit, bool set)
@@ -945,15 +982,37 @@ public sealed class VirtualMachine
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 = Volatile.Read(ref _heldInputCallbackMask)
_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
while (_cur.InputCallbackScanIndex < 32)
{
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;
@@ -965,6 +1024,11 @@ public sealed class VirtualMachine
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;
@@ -983,6 +1047,22 @@ public sealed class VirtualMachine
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;
}
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":