Implement script-configured input bindings
This commit is contained in:
@@ -9,7 +9,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
| `0x40c` | `sys4ini_count` | `int` | SYS4INI record count |
|
||||
| `0x410` | `archive_name_table` | `void*` | archive-name table base (arc_id*0x100 indexes it) |
|
||||
| `0x414` | `sys4ini_records` | `void*` | SYS4INI 80-byte record base {name[64],arc_id,file_number,offset,size}; record = base + id*0x50 |
|
||||
| `0x814` | `input_action_count` | `int` | logical input callback count (0..31); op 0xfe sets it and ops 0xff/0x100 bound their action-mask scan by it |
|
||||
| `0x814` | `input_action_count` | `int` | logical action count (0..31); op 0xfe sets it, op 0x100 scans actions below it and uses callback slot count itself when the polled mask is empty |
|
||||
| `0x898` | `joystick_physical_button_count` | `int` | WinMM JOYCAPS physical button count returned by op 0x106 |
|
||||
| `0x89c` | `joystick_button_map` | `int` | base of 32-entry logical button-slot to physical joystick-button table; op 0x107 writes it, slot N emits action N+4 |
|
||||
| `0x135c` | `mouse_button_action_map` | `int` | base of physical mouse-button to logical button-slot table; op 0x10b writes it and polling adds 4 to obtain the action index |
|
||||
@@ -71,6 +71,8 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
| `0x5f72c` | `adv_wait_indicator_frame` | `int` | current animated input-wait indicator frame, reset to zero when the service is armed |
|
||||
| `0x5f730` | `adv_wait_indicator_terminal_frame` | `int` | terminal frame returned by the indicator layout during ADV wait setup |
|
||||
| `0x5f734` | `adv_wait_indicator_enabled_value` | `int` | raw enable operand last written by op 0x1ce; nonzero arms the ADV wait-indicator service |
|
||||
| `0x6da6c` | `input_callback_scan_index` | `int` | op 0xff resets this to zero; op 0x100 advances it while scanning set action bits below input_action_count |
|
||||
| `0x6da70` | `input_action_count_snapshot` | `int` | op 0xff copies input_action_count here when beginning a callback poll/dispatch pass |
|
||||
| `0x6da78` | `message_skip_activation_guard` | `int` | set when op 0x88 enables Skip; suppresses the enabling click from immediately becoming a hotspot activation |
|
||||
| `0x6da80` | `message_skip_cancel_click_phase` | `int` | press/release phase used only when set:CancelMesSkipOnClick is nonzero |
|
||||
| `0x6da84` | `adv_wait_indicator_layout_slot` | `int` | ADV text layout whose wait indicator is armed or erased by input-wait setup and op 0x1ce |
|
||||
|
||||
@@ -2273,7 +2273,8 @@ poll/dispatch ops `0xff` and `0x100`. These calls are effectful engine configura
|
||||
|
||||
- `op_0xfe_set_input_action_count@0x421390` stores an unsigned count below 32 at EngineCtx `+0x814` and
|
||||
throws script error `0x10005` for an invalid count. SYSTEM4 sets 10, so callback dispatch scans actions
|
||||
0 through 9.
|
||||
0 through 9. If the polled mask is empty, `op_0x100_dispatch_joy_callbacks@0x416f00` instead invokes
|
||||
callback slot 10 and resumes after itself; that slot is the native no-input/release path, not action 10.
|
||||
- `op_0x107_map_joystick_button@0x421550` writes physical joystick button numbers into the 32-entry
|
||||
EngineCtx `+0x89c` table. Joystick axes directly emit actions 0=up, 1=right, 2=down, 3=left; table slot
|
||||
N emits action N+4. SYSTEM4 maps physical buttons 0,3,2,1,6,7 to actions 4 through 9.
|
||||
@@ -2282,22 +2283,25 @@ poll/dispatch ops `0xff` and `0x100`. These calls are effectful engine configura
|
||||
making left mouse action 4 by default; SYSTEM4 `(3,1)` maps right mouse to action 7.
|
||||
- `op_0x10c_map_keyboard_scancode@0x421730` translates a DirectInput DIK scan code through EngineCtx
|
||||
`+0x1828` to a Win32 virtual key, then writes the action into the 256-entry table at `+0x1428`.
|
||||
Initialization prebinds the arrow keys to actions 0=up, 1=right, 2=down, 3=left. SYSTEM4 adds action 4
|
||||
= Z/Enter, 5 = Space, 6 = C/LeftCtrl, 7 = X, 8 = PageUp, and 9 = PageDown.
|
||||
Initialization sets count 7 and prebinds 0=up, 1=right, 2=down, 3=left, 4=Enter, 5=Space, and
|
||||
6=Backspace. SYSTEM4 raises the count to 10 and adds Z to action 4, C/LeftCtrl to action 6, X to action
|
||||
7, PageUp to action 8, and PageDown to action 9; the earlier Enter/Space/Backspace mappings remain.
|
||||
|
||||
`input_poll_action_mask@0x4608b0` combines
|
||||
`input_poll_keyboard_action_bits@0x4601a0`, `input_poll_mouse_action_bits@0x460240`, and
|
||||
`input_poll_joystick_action_bits@0x460380`. The first three logical actions are therefore not a generic
|
||||
Godot UI ordering; they are the native engine ABI established by the input manager and refined by scripts.
|
||||
|
||||
The current port bypasses this configuration. `godot/Main.cs` feeds `ui_down`, `ui_left`, `ui_up`, and
|
||||
`ui_right` directly into indices 0,1,2,3, then maps `ui_accept`/`ui_cancel` to 4/5. The VM also scans all
|
||||
32 callback indices instead of the configured count. As a result the directional ordering differs from
|
||||
native, auxiliary actions 6 through 9 and native right-mouse action 7 are not physically reachable through
|
||||
the SYSTEM4 bindings, and the four configuration opcodes still fall back. A faithful implementation should
|
||||
add one engine-owned input-binding model shared by these four handlers and `0xff`/`0x100`, then translate
|
||||
Godot key, mouse, and joy events through it. This is frontend input configuration only: it needs no profile
|
||||
storage, boot seed, or game-specific conditional.
|
||||
The port now models this as one process-owned `InputBindings` service. It starts with the native seven
|
||||
keyboard defaults, the four configuration handlers mutate it, and `0xff` combines live keyboard-VK,
|
||||
left/right mouse, joystick-axis, and joystick-button state with the narrow logical injection used by tests.
|
||||
`0x100` scans only actions below the configured count, resumes on itself for simultaneous held actions, and
|
||||
uses callback slot `count` only for an empty mask. Godot translates layout-independent physical keys to the
|
||||
native Win32 VK namespace and sends physical mouse/standard joy events through the service; it no longer
|
||||
assigns Godot's `ui_*` actions directly to AGE indices. Direct-scene diagnostics replay the same 16 immediate
|
||||
SYSTEM4 configuration calls through `InputBindingBootstrap`, while natural boot executes the real opcodes.
|
||||
This remains frontend input configuration only: it needs no profile storage, boot seed, or game-specific
|
||||
conditional.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -721,19 +721,19 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **evidence:** Ghidra /v2: op_0xfb_register_joy_callback@0x421270 bounds-checks operand 1 to 0..31 and stores operand 2 in the current script frame's 33-entry callback table. Ops 0xff/0x100 poll and dispatch this table; HISTORY.BIN and HIDEWIN.BIN register indices 0..10.
|
||||
|
||||
### 0xfe `set-input-action-count` (u0041E360, argc 1)
|
||||
- **summary:** (count) - set the number of logical input actions scanned and dispatched by ops 0xff/0x100.
|
||||
- **summary:** (count) - set the logical action scan bound; op 0x100 uses callback slot count itself when the polled mask is empty.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0xfe_set_input_action_count@0x421390 accepts unsigned values below 32, stores the count at EngineCtx+0x814, and throws script error 0x10005 otherwise. op_0xff_poll_joy_callback_input passes the input-manager subobject to input_poll_action_mask@0x4608b0 with this count as its scan bound; op_0x100_dispatch_joy_callbacks rejects selected action indices at or above the same count. SYSTEM4@0x1cf sets 10, enabling logical actions 0..9 before configuring joystick, mouse, and keyboard mappings.
|
||||
- **evidence:** Ghidra /v2: op_0xfe_set_input_action_count@0x421390 accepts unsigned values below 32, stores the count at EngineCtx+0x814, and throws script error 0x10005 otherwise. op_0x100_dispatch_joy_callbacks scans set mask bits only below this count; when op 0xff produces an empty mask, it instead dispatches the callback table entry at index count and resumes after op 0x100. SYSTEM4@0x1cf sets 10, enabling held logical actions 0..9 and reserving callback slot 10 for the no-input/release path before configuring joystick, mouse, and keyboard mappings.
|
||||
|
||||
### 0xff `poll-joy-callback-input` (u00415A10, argc 0)
|
||||
- **summary:** Poll the current joy/input callback bitmask and initialize the per-dispatch scan state.
|
||||
- **summary:** Poll the configured keyboard/mouse/joystick action mask and initialize the op 0x100 dispatch scan.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0xff_poll_joy_callback_input@0x416eb0 clears the pending input mask, fills it through input_poll_action_mask@0x4608b0, resets the scan index, and snapshots the current input selector. That poller combines configured keyboard, mouse-action, and joystick bits, bounded by EngineCtx+0x814 input_action_count. input_poll_mouse_action_bits@0x460240 maps VK_LBUTTON/VK_RBUTTON through mouse_button_action_map and adds 4; initialization leaves both slots zero, so left defaults to action 4, while SYSTEM4 op 0x10b remaps right to action 7. It pairs with op 0x100.
|
||||
- **evidence:** Ghidra /v2: op_0xff_poll_joy_callback_input@0x416eb0 clears the pending input mask, fills it through input_poll_action_mask@0x4608b0, resets EngineCtx+0x6da6c input_callback_scan_index, and copies input_action_count to +0x6da70. The poller combines configured keyboard, mouse-action, and joystick bits; the joystick button-slot loop uses input_action_count, while op 0x100 applies the final action scan bound. input_poll_mouse_action_bits@0x460240 maps VK_LBUTTON/VK_RBUTTON through mouse_button_action_map and adds 4; initialization leaves both slots zero, so left defaults to action 4, while SYSTEM4 op 0x10b remaps right to action 7.
|
||||
|
||||
### 0x100 `dispatch-joy-callbacks` (u00415A60, argc 0)
|
||||
- **summary:** Dispatch registered callbacks for the current or pending joy/input selection.
|
||||
- **summary:** Dispatch each set logical action below the configured count, or callback slot count when no action is held.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x100_dispatch_joy_callbacks@0x416f00 scans the bitmask captured by op 0xff (or uses the current selector when no mask is present), bounded by EngineCtx+0x814 input_action_count; it pushes a return PC and jumps through the current frame's callback table populated by op 0xfb.
|
||||
- **evidence:** Ghidra /v2: op_0x100_dispatch_joy_callbacks@0x416f00 scans the bitmask captured by op 0xff from EngineCtx+0x6da6c up to but excluding EngineCtx+0x814 input_action_count. Each set action resumes at op 0x100 so simultaneous bits continue dispatching. If the mask is empty, it dispatches callback table slot input_action_count and resumes after op 0x100. Callback targets come from the current frame's table populated by op 0xfb; SYSTEM4's count 10 therefore makes slot 10 the native no-input/release callback rather than an eleventh action bit.
|
||||
|
||||
### 0x101 `reset-message-skip-input` (u00415BF0, argc 0)
|
||||
- **summary:** Reset transient message-skip/input service state after an ADV chrome action without clearing op 0x88's persistent all-message Skip flag.
|
||||
@@ -768,7 +768,7 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
### 0x10c `map-keyboard-scancode` (u0041E5E0, argc 2)
|
||||
- **summary:** (action)(dik_scan_code) - map a DirectInput keyboard scan code to a logical input action.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x10c_map_keyboard_scancode@0x421730 requires unsigned operand1<32 or throws script error 0x10005. It translates operand 2 through EngineCtx+0x1828, the DIK-to-Win32-VK table built by input_initialize_dik_to_vk_table@0x45fc60, then writes the action to EngineCtx+0x1428[VK]. input_poll_keyboard_action_bits@0x4601a0 polls 256 virtual keys and emits the configured action bits. input_manager_initialize_defaults@0x460630 prebinds arrow keys as 0=up, 1=right, 2=down, 3=left. SYSTEM4 additionally maps action 4 to Z/Enter, 5 to Space, 6 to C/LeftCtrl, 7 to X, 8 to PageUp, and 9 to PageDown.
|
||||
- **evidence:** Ghidra /v2: op_0x10c_map_keyboard_scancode@0x421730 requires unsigned operand1<32 or throws script error 0x10005. It translates operand 2 through EngineCtx+0x1828, the DIK-to-Win32-VK table built by input_initialize_dik_to_vk_table@0x45fc60, then writes the action to EngineCtx+0x1428[VK]. input_poll_keyboard_action_bits@0x4601a0 polls 256 virtual keys and emits the configured action bits. input_manager_initialize_defaults@0x460630 starts with count 7 and maps 0=Up, 1=Right, 2=Down, 3=Left, 4=Enter, 5=Space, and 6=Backspace. SYSTEM4 raises the count to 10 and adds Z to action 4, C/LeftCtrl to 6, X to 7, PageUp to 8, and PageDown to 9 without erasing the earlier mappings.
|
||||
|
||||
### 0x10d `consume-mouse-wheel-delta` (u00415F10, argc 1)
|
||||
- **summary:** (out) - return the accumulated signed mouse-wheel delta and clear it.
|
||||
|
||||
@@ -2170,3 +2170,23 @@ claimed by this investigation.
|
||||
|
||||
**Next:** implement that input-binding service as one coherent slice with focused mapping, bounds, and
|
||||
simultaneous-input regressions, then manually exercise TITLE/ROOM/ADV keyboard and mouse paths.
|
||||
|
||||
### Slice B1 SYSTEM4 logical input bindings implemented (2026-07-21)
|
||||
|
||||
`InputBindings` now owns AGE's process-wide physical-to-logical map. Its constructor reproduces native
|
||||
count 7 and the retained defaults Up/Right/Down/Left, Enter, Space, and Backspace. Opcodes
|
||||
`0xfe`/`0x107`/`0x10b`/`0x10c` mutate the action bound, joystick slots, mouse slots, and DIK-translated
|
||||
Win32-VK map. Godot supplies physical key, left/right mouse, standardized joy-button, and primary-axis
|
||||
state instead of hardcoded `ui_*` indices; direct-scene diagnostics replay SYSTEM4's 16 immediate mapping
|
||||
calls through a bounded bootstrap while natural boot executes them normally.
|
||||
|
||||
The implementation also corrects `0x100`'s release boundary after a final native trace: set bits dispatch
|
||||
only below `input_action_count` and resume at `0x100` for simultaneous inputs, while an empty mask dispatches
|
||||
callback slot `input_action_count` and resumes after the opcode. SYSTEM4's value 10 therefore reserves slot
|
||||
10 as the no-input/release callback rather than exposing action bit 10. The old synthetic release-bit queue
|
||||
is no longer used by Godot. Focused tests cover native defaults, all four setters, the real SYSTEM4 block,
|
||||
empty-mask dispatch, simultaneous/bounded actions, and native range failures.
|
||||
|
||||
**Next:** manually exercise TITLE, ROOM, and ADV with keyboard, left/right mouse, and—if available—a
|
||||
controller. A clean check should confirm native direction order, Z/Enter/Space behavior, right-click action
|
||||
7, and release re-arming before selecting another reached Phase-B cluster.
|
||||
|
||||
@@ -286,9 +286,10 @@ boundaries. `AdvTextHistory`, VM dispatch, the direct-scene SYSTEM4 bootstrap, a
|
||||
label geometry now share that script-owned state; the former slot-1 hardcoding is removed. The next
|
||||
reached SYSTEM4 cluster is also resolved: `0xfe` establishes ten logical input actions, while `0x107`,
|
||||
`0x10b`, and `0x10c` configure joystick-button, mouse-button, and DIK keyboard mappings consumed by
|
||||
`0xff`/`0x100`. The port currently bypasses that script-owned map with six hardcoded Godot action indices
|
||||
and scans 32 callbacks, so the next coherent slice is an engine-owned input-binding model plus these four
|
||||
handlers and physical Godot event translation. This is independent of profile/save ownership. Closing
|
||||
`0xff`/`0x100`. The port now owns the native seven-action defaults, executes all four setters, translates
|
||||
Godot physical key/mouse/joy events through the resulting map, scans only actions below the configured
|
||||
count, and invokes callback slot `count` for the empty-mask release path. Direct-scene diagnostics replay
|
||||
SYSTEM4's same 16 immediate configuration calls. This is independent of profile/save ownership. Closing
|
||||
Godot currently releases a parked ADV wait before process teardown, so the page map may contain one trailing
|
||||
shutdown-only page; the final timeline `input-wait` is the authoritative stop.
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
135
engine/Age.Engine.Tests/InputBindingTests.cs
Normal file
135
engine/Age.Engine.Tests/InputBindingTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
199
engine/Age.Engine/Model/InputBindings.cs
Normal file
199
engine/Age.Engine/Model/InputBindings.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
38
engine/Age.Engine/Vm/InputBindingBootstrap.cs
Normal file
38
engine/Age.Engine/Vm/InputBindingBootstrap.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
@@ -240,8 +240,11 @@ public partial class Main : Godot.Control
|
||||
// alongside the inherited SO000/SO001 state below. Full Phase-B SYSTEM4 replay will replace this
|
||||
// bootstrap as one unit; HISTORY depends on the 650x150 dimensions of layouts 2..6 for clipping.
|
||||
if (directSceneHarness)
|
||||
AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets(
|
||||
scripts!.RequireByName("SYSTEM4.BIN"), table, _vm.TextHistory);
|
||||
{
|
||||
var systemScript = scripts!.RequireByName("SYSTEM4.BIN");
|
||||
AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets(systemScript, table, _vm.TextHistory);
|
||||
InputBindingBootstrap.Apply(systemScript, _vm.InputBindings);
|
||||
}
|
||||
// SYSTEM4 loads the shared SO001 chrome sheet into surface slot 17 before any scene runs.
|
||||
// Seed that inherited retained-surface state without replaying the entrypoint's unrelated UI flow.
|
||||
if (directSceneHarness && resources.ResolveName("SO001.AGF") is { } systemChrome)
|
||||
@@ -418,18 +421,17 @@ public partial class Main : Godot.Control
|
||||
bool rawInputCallbackActive = _vm.IsRawInputCallbackActive;
|
||||
_vm.UpdatePointer(p.X, p.Y);
|
||||
int nativeButtonBit = mb.ButtonIndex == MouseButton.Left ? 0x1 : 0x2;
|
||||
int physicalButton = mb.ButtonIndex == MouseButton.Left ? 0 : 1;
|
||||
_vm.UpdateMouseButtonState(nativeButtonBit, mb.Pressed);
|
||||
_vm.UpdatePhysicalMouseButtonState(physicalButton, mb.Pressed);
|
||||
if (mb.Pressed && _host.IsModalMovieWaiting)
|
||||
{
|
||||
_host.SignalInput();
|
||||
GetViewport().SetInputAsHandled();
|
||||
return;
|
||||
}
|
||||
// AGE exposes the physical left button twice: raw mask 0x1 for the timed mouse callback,
|
||||
// and the configured primary action (default input callback index 4). Script-owned callback
|
||||
// loops consume both channels without releasing the enclosing ADV page wait.
|
||||
if (mb.ButtonIndex == MouseButton.Left && (advPageSuspended || rawInputCallbackActive))
|
||||
_vm.QueueInputCallback(mb.Pressed ? 4 : 10);
|
||||
// AGE exposes mouse buttons twice: op 0x108 reads the raw bitmask while op 0xff translates
|
||||
// the held physical button through the script-configured logical action map.
|
||||
if (mb.ButtonIndex == MouseButton.Left && mb.Pressed && _vm.TryActivatePointer(p.X, p.Y))
|
||||
{
|
||||
GetViewport().SetInputAsHandled();
|
||||
@@ -441,32 +443,43 @@ public partial class Main : Godot.Control
|
||||
&& !advPageSuspended && !rawInputCallbackActive) _host.SignalInput();
|
||||
return;
|
||||
}
|
||||
UpdateAgeInputCallback(e, "ui_down", 0);
|
||||
UpdateAgeInputCallback(e, "ui_left", 1);
|
||||
UpdateAgeInputCallback(e, "ui_up", 2);
|
||||
UpdateAgeInputCallback(e, "ui_right", 3);
|
||||
UpdateAgeInputCallback(e, "ui_accept", 4);
|
||||
UpdateAgeInputCallback(e, "ui_cancel", 5);
|
||||
if (_host.IsModalMovieWaiting
|
||||
&& (e.IsActionPressed("ui_accept") || e.IsActionPressed("ui_cancel")))
|
||||
if (e is InputEventKey gameplayKey && !gameplayKey.Echo
|
||||
&& Win32VirtualKeyTranslator.TryTranslate(gameplayKey, out int virtualKey))
|
||||
{
|
||||
int action = _vm.UpdateKeyboardVirtualKeyState(virtualKey, gameplayKey.Pressed);
|
||||
if (gameplayKey.Pressed && IsAdvanceAction(action))
|
||||
{
|
||||
if (_host.IsModalMovieWaiting)
|
||||
{
|
||||
_host.SignalInput();
|
||||
GetViewport().SetInputAsHandled();
|
||||
}
|
||||
else if (!_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive)
|
||||
_host.SignalInput();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.IsActionPressed("ui_accept")
|
||||
&& !_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive) _host.SignalInput();
|
||||
if (e is InputEventJoypadButton joyButton)
|
||||
{
|
||||
int actionMask = _vm.UpdateJoystickButtonState((int)joyButton.ButtonIndex, joyButton.Pressed);
|
||||
if (joyButton.Pressed && HasAdvanceAction(actionMask))
|
||||
{
|
||||
if (_host.IsModalMovieWaiting)
|
||||
{
|
||||
_host.SignalInput();
|
||||
GetViewport().SetInputAsHandled();
|
||||
}
|
||||
else if (!_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive)
|
||||
_host.SignalInput();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e is InputEventJoypadMotion joyMotion && (int)joyMotion.Axis is 0 or 1)
|
||||
_vm.UpdateJoystickAxisState((int)joyMotion.Axis, joyMotion.AxisValue);
|
||||
}
|
||||
|
||||
private void UpdateAgeInputCallback(InputEvent e, StringName action, int index)
|
||||
{
|
||||
if (e.IsActionPressed(action)) _vm.UpdateInputCallbackState(index, true);
|
||||
if (e.IsActionReleased(action))
|
||||
{
|
||||
_vm.UpdateInputCallbackState(index, false);
|
||||
_vm.QueueInputCallback(10); // common release callback registered by HISTORY/HIDEWIN
|
||||
}
|
||||
}
|
||||
private static bool IsAdvanceAction(int action) => action is 4 or 5;
|
||||
private static bool HasAdvanceAction(int mask) => (mask & ((1 << 4) | (1 << 5))) != 0;
|
||||
|
||||
private void ToggleDebugSceneLauncher()
|
||||
{
|
||||
@@ -1217,11 +1230,19 @@ public partial class Main : Godot.Control
|
||||
launcherSmoke.QueueFree();
|
||||
bool sleepMinimumOk = GodotAdvHost.NormalizeSleepMilliseconds(0, 1.0) == 1
|
||||
&& GodotAdvHost.NormalizeSleepMilliseconds(100, 1.0) == 100;
|
||||
ok &= launcherOk && sleepMinimumOk;
|
||||
bool inputTranslationOk = Win32VirtualKeyTranslator.TryTranslate(
|
||||
new InputEventKey { PhysicalKeycode = Key.Z }, out int zVk) && zVk == 0x5a
|
||||
&& Win32VirtualKeyTranslator.TryTranslate(
|
||||
new InputEventKey { PhysicalKeycode = Key.Up }, out int upVk) && upVk == 0x26
|
||||
&& Win32VirtualKeyTranslator.TryTranslate(
|
||||
new InputEventKey { PhysicalKeycode = Key.Ctrl }, out int ctrlVk) && ctrlVk == 0x11;
|
||||
ok &= launcherOk && sleepMinimumOk && inputTranslationOk;
|
||||
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
||||
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); sleep-min=1ms");
|
||||
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
|
||||
$"sleep-min=1ms; native-key-translation=ok");
|
||||
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
|
||||
$"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}");
|
||||
$"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}; " +
|
||||
$"native-key-translation={inputTranslationOk}");
|
||||
GetTree().Quit(ok ? 0 : 1);
|
||||
}
|
||||
|
||||
|
||||
88
godot/Win32VirtualKeyTranslator.cs
Normal file
88
godot/Win32VirtualKeyTranslator.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using Godot;
|
||||
|
||||
/// <summary>Translate Godot's layout-independent physical key identity into the Win32 VK namespace
|
||||
/// used by AGE's native DIK translation table and GetAsyncKeyState poller.</summary>
|
||||
internal static class Win32VirtualKeyTranslator
|
||||
{
|
||||
public static bool TryTranslate(InputEventKey input, out int virtualKey)
|
||||
{
|
||||
Key key = input.PhysicalKeycode is not (Key.None or Key.Unknown)
|
||||
? input.PhysicalKeycode : input.Keycode;
|
||||
long value = (long)key;
|
||||
if (value is >= (long)Key.Key0 and <= (long)Key.Key9
|
||||
|| value is >= (long)Key.A and <= (long)Key.Z)
|
||||
{
|
||||
virtualKey = (int)value;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtualKey = key switch
|
||||
{
|
||||
Key.Backspace => 0x08,
|
||||
Key.Tab => 0x09,
|
||||
Key.Enter or Key.KpEnter => 0x0d,
|
||||
Key.Shift => 0x10,
|
||||
Key.Ctrl => 0x11,
|
||||
Key.Alt => 0x12,
|
||||
Key.Pause => 0x13,
|
||||
Key.Capslock => 0x14,
|
||||
Key.Escape => 0x1b,
|
||||
Key.Space => 0x20,
|
||||
Key.Pageup => 0x21,
|
||||
Key.Pagedown => 0x22,
|
||||
Key.End => 0x23,
|
||||
Key.Home => 0x24,
|
||||
Key.Left => 0x25,
|
||||
Key.Up => 0x26,
|
||||
Key.Right => 0x27,
|
||||
Key.Down => 0x28,
|
||||
Key.Print or Key.Sysreq => 0x2c,
|
||||
Key.Insert => 0x2d,
|
||||
Key.Delete => 0x2e,
|
||||
Key.Meta => input.Location == KeyLocation.Right ? 0x5c : 0x5b,
|
||||
Key.Menu => 0x5d,
|
||||
Key.Kp0 => 0x60,
|
||||
Key.Kp1 => 0x61,
|
||||
Key.Kp2 => 0x62,
|
||||
Key.Kp3 => 0x63,
|
||||
Key.Kp4 => 0x64,
|
||||
Key.Kp5 => 0x65,
|
||||
Key.Kp6 => 0x66,
|
||||
Key.Kp7 => 0x67,
|
||||
Key.Kp8 => 0x68,
|
||||
Key.Kp9 => 0x69,
|
||||
Key.KpMultiply => 0x6a,
|
||||
Key.KpAdd => 0x6b,
|
||||
Key.KpSubtract => 0x6d,
|
||||
Key.KpPeriod => 0x6e,
|
||||
Key.KpDivide => 0x6f,
|
||||
Key.F1 => 0x70,
|
||||
Key.F2 => 0x71,
|
||||
Key.F3 => 0x72,
|
||||
Key.F4 => 0x73,
|
||||
Key.F5 => 0x74,
|
||||
Key.F6 => 0x75,
|
||||
Key.F7 => 0x76,
|
||||
Key.F8 => 0x77,
|
||||
Key.F9 => 0x78,
|
||||
Key.F10 => 0x79,
|
||||
Key.F11 => 0x7a,
|
||||
Key.F12 => 0x7b,
|
||||
Key.Numlock => 0x90,
|
||||
Key.Scrolllock => 0x91,
|
||||
Key.Semicolon or Key.Colon => 0xba,
|
||||
Key.Equal or Key.Plus => 0xbb,
|
||||
Key.Comma or Key.Less => 0xbc,
|
||||
Key.Minus or Key.Underscore => 0xbd,
|
||||
Key.Period or Key.Greater => 0xbe,
|
||||
Key.Slash or Key.Question => 0xbf,
|
||||
Key.Quoteleft or Key.Asciitilde => 0xc0,
|
||||
Key.Bracketleft or Key.Braceleft => 0xdb,
|
||||
Key.Backslash or Key.Bar => 0xdc,
|
||||
Key.Bracketright or Key.Braceright => 0xdd,
|
||||
Key.Apostrophe or Key.Quotedbl => 0xde,
|
||||
_ => 0,
|
||||
};
|
||||
return virtualKey != 0;
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,9 @@ INFERRED: dict[int, dict] = {
|
||||
0xd5: dict(name='run-timed-callback-sequence', category='control', noop=False, confidence='high', source='investigation', summary='Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late.'),
|
||||
0xd9: dict(name='clear-run-state-0x1000', category='control', noop=True, confidence='high', source='investigation', summary='Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.'),
|
||||
0xfb: dict(name='register-joy-callback', category='input', noop=False, confidence='high', source='investigation', summary='(input_index)(target_pc) - register one of 32 per-frame joy/input callback targets.'),
|
||||
0xfe: dict(name='set-input-action-count', category='input', noop=False, confidence='high', source='investigation', summary='(count) - set the number of logical input actions scanned and dispatched by ops 0xff/0x100.'),
|
||||
0xff: dict(name='poll-joy-callback-input', category='input', noop=False, confidence='high', source='investigation', summary='Poll the current joy/input callback bitmask and initialize the per-dispatch scan state.'),
|
||||
0x100: dict(name='dispatch-joy-callbacks', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch registered callbacks for the current or pending joy/input selection.'),
|
||||
0xfe: dict(name='set-input-action-count', category='input', noop=False, confidence='high', source='investigation', summary='(count) - set the logical action scan bound; op 0x100 uses callback slot count itself when the polled mask is empty.'),
|
||||
0xff: dict(name='poll-joy-callback-input', category='input', noop=False, confidence='high', source='investigation', summary='Poll the configured keyboard/mouse/joystick action mask and initialize the op 0x100 dispatch scan.'),
|
||||
0x100: dict(name='dispatch-joy-callbacks', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch each set logical action below the configured count, or callback slot count when no action is held.'),
|
||||
0x101: dict(name='reset-message-skip-input', category='input', noop=False, confidence='med', source='investigation', summary="Reset transient message-skip/input service state after an ADV chrome action without clearing op 0x88's persistent all-message Skip flag."),
|
||||
0x107: dict(name='map-joystick-button', category='input', noop=False, confidence='high', source='investigation', summary='(button_slot)(physical_button) - map a logical button slot to a physical joystick button; slot N emits logical action N+4.'),
|
||||
0x108: dict(name='get-mouse-button-state', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current mouse-button state bitmask.'),
|
||||
|
||||
@@ -26,7 +26,7 @@ note = "SYS4INI 80-byte record base {name[64],arc_id,file_number,offset,size}; r
|
||||
offset = 0x814
|
||||
name = "input_action_count"
|
||||
type = "int"
|
||||
note = "logical input callback count (0..31); op 0xfe sets it and ops 0xff/0x100 bound their action-mask scan by it"
|
||||
note = "logical action count (0..31); op 0xfe sets it, op 0x100 scans actions below it and uses callback slot count itself when the polled mask is empty"
|
||||
[[field]]
|
||||
offset = 0x898
|
||||
name = "joystick_physical_button_count"
|
||||
@@ -333,6 +333,16 @@ name = "adv_wait_indicator_enabled_value"
|
||||
type = "int"
|
||||
note = "raw enable operand last written by op 0x1ce; nonzero arms the ADV wait-indicator service"
|
||||
[[field]]
|
||||
offset = 0x6da6c
|
||||
name = "input_callback_scan_index"
|
||||
type = "int"
|
||||
note = "op 0xff resets this to zero; op 0x100 advances it while scanning set action bits below input_action_count"
|
||||
[[field]]
|
||||
offset = 0x6da70
|
||||
name = "input_action_count_snapshot"
|
||||
type = "int"
|
||||
note = "op 0xff copies input_action_count here when beginning a callback poll/dispatch pass"
|
||||
[[field]]
|
||||
offset = 0x6da78
|
||||
name = "message_skip_activation_guard"
|
||||
type = "int"
|
||||
|
||||
@@ -2247,12 +2247,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "set-input-action-count"
|
||||
category = "input"
|
||||
summary = "(count) - set the number of logical input actions scanned and dispatched by ops 0xff/0x100."
|
||||
summary = "(count) - set the logical action scan bound; op 0x100 uses callback slot count itself when the polled mask is empty."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra /v2: op_0xfe_set_input_action_count@0x421390 accepts unsigned values below 32, stores the count at EngineCtx+0x814, and throws script error 0x10005 otherwise. op_0xff_poll_joy_callback_input passes the input-manager subobject to input_poll_action_mask@0x4608b0 with this count as its scan bound; op_0x100_dispatch_joy_callbacks rejects selected action indices at or above the same count. SYSTEM4@0x1cf sets 10, enabling logical actions 0..9 before configuring joystick, mouse, and keyboard mappings."
|
||||
evidence = "Ghidra /v2: op_0xfe_set_input_action_count@0x421390 accepts unsigned values below 32, stores the count at EngineCtx+0x814, and throws script error 0x10005 otherwise. op_0x100_dispatch_joy_callbacks scans set mask bits only below this count; when op 0xff produces an empty mask, it instead dispatches the callback table entry at index count and resumes after op 0x100. SYSTEM4@0x1cf sets 10, enabling held logical actions 0..9 and reserving callback slot 10 for the no-input/release path before configuring joystick, mouse, and keyboard mappings."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
@@ -2268,12 +2268,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "poll-joy-callback-input"
|
||||
category = "input"
|
||||
summary = "Poll the current joy/input callback bitmask and initialize the per-dispatch scan state."
|
||||
summary = "Poll the configured keyboard/mouse/joystick action mask and initialize the op 0x100 dispatch scan."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra /v2: op_0xff_poll_joy_callback_input@0x416eb0 clears the pending input mask, fills it through input_poll_action_mask@0x4608b0, resets the scan index, and snapshots the current input selector. That poller combines configured keyboard, mouse-action, and joystick bits, bounded by EngineCtx+0x814 input_action_count. input_poll_mouse_action_bits@0x460240 maps VK_LBUTTON/VK_RBUTTON through mouse_button_action_map and adds 4; initialization leaves both slots zero, so left defaults to action 4, while SYSTEM4 op 0x10b remaps right to action 7. It pairs with op 0x100."
|
||||
evidence = "Ghidra /v2: op_0xff_poll_joy_callback_input@0x416eb0 clears the pending input mask, fills it through input_poll_action_mask@0x4608b0, resets EngineCtx+0x6da6c input_callback_scan_index, and copies input_action_count to +0x6da70. The poller combines configured keyboard, mouse-action, and joystick bits; the joystick button-slot loop uses input_action_count, while op 0x100 applies the final action scan bound. input_poll_mouse_action_bits@0x460240 maps VK_LBUTTON/VK_RBUTTON through mouse_button_action_map and adds 4; initialization leaves both slots zero, so left defaults to action 4, while SYSTEM4 op 0x10b remaps right to action 7."
|
||||
|
||||
[[opcode]]
|
||||
op = 0x100
|
||||
@@ -2284,12 +2284,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "dispatch-joy-callbacks"
|
||||
category = "input"
|
||||
summary = "Dispatch registered callbacks for the current or pending joy/input selection."
|
||||
summary = "Dispatch each set logical action below the configured count, or callback slot count when no action is held."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra /v2: op_0x100_dispatch_joy_callbacks@0x416f00 scans the bitmask captured by op 0xff (or uses the current selector when no mask is present), bounded by EngineCtx+0x814 input_action_count; it pushes a return PC and jumps through the current frame's callback table populated by op 0xfb."
|
||||
evidence = "Ghidra /v2: op_0x100_dispatch_joy_callbacks@0x416f00 scans the bitmask captured by op 0xff from EngineCtx+0x6da6c up to but excluding EngineCtx+0x814 input_action_count. Each set action resumes at op 0x100 so simultaneous bits continue dispatching. If the mask is empty, it dispatches callback table slot input_action_count and resumes after op 0x100. Callback targets come from the current frame's table populated by op 0xfb; SYSTEM4's count 10 therefore makes slot 10 the native no-input/release callback rather than an eleventh action bit."
|
||||
|
||||
[[opcode]]
|
||||
op = 0x101
|
||||
@@ -2446,7 +2446,7 @@ noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra /v2: op_0x10c_map_keyboard_scancode@0x421730 requires unsigned operand1<32 or throws script error 0x10005. It translates operand 2 through EngineCtx+0x1828, the DIK-to-Win32-VK table built by input_initialize_dik_to_vk_table@0x45fc60, then writes the action to EngineCtx+0x1428[VK]. input_poll_keyboard_action_bits@0x4601a0 polls 256 virtual keys and emits the configured action bits. input_manager_initialize_defaults@0x460630 prebinds arrow keys as 0=up, 1=right, 2=down, 3=left. SYSTEM4 additionally maps action 4 to Z/Enter, 5 to Space, 6 to C/LeftCtrl, 7 to X, 8 to PageUp, and 9 to PageDown."
|
||||
evidence = "Ghidra /v2: op_0x10c_map_keyboard_scancode@0x421730 requires unsigned operand1<32 or throws script error 0x10005. It translates operand 2 through EngineCtx+0x1828, the DIK-to-Win32-VK table built by input_initialize_dik_to_vk_table@0x45fc60, then writes the action to EngineCtx+0x1428[VK]. input_poll_keyboard_action_bits@0x4601a0 polls 256 virtual keys and emits the configured action bits. input_manager_initialize_defaults@0x460630 starts with count 7 and maps 0=Up, 1=Right, 2=Down, 3=Left, 4=Enter, 5=Space, and 6=Backspace. SYSTEM4 raises the count to 10 and adds Z to action 4, C/LeftCtrl to 6, X to 7, PageUp to 8, and PageDown to 9 without erasing the earlier mappings."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
|
||||
Reference in New Issue
Block a user