Implement script-configured input bindings

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

View File

@@ -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))
{
_host.SignalInput();
GetViewport().SetInputAsHandled();
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);
}

View 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;
}
}