using System; using System.Collections.Generic; using Godot; using Age.Engine.Diagnostics; using Age.Engine.Vm; public partial class Main { private ImageTexture? _ageCursorTexture; private Label _locatorHud = null!; private FullwidthTextEditorDialog? _fullwidthTextEditor; private DebugSceneLauncher? _debugSceneLauncher; private IReadOnlyList _debugSceneEntries = System.Array.Empty(); private PageLocatorState _locator = null!; private bool _locatorHudVisible; // _Input (not _UnhandledInput): the root Control consumes mouse clicks as GUI input before they // reach _UnhandledInput, so clicks were swallowed while keyboard ui_accept still got through. public override void _Input(InputEvent e) { if (_selftest) return; if (e is InputEventKey key && key.Pressed && !key.Echo && key.Keycode == Key.F2) { _locatorHudVisible = !_locatorHudVisible; _locatorHud.Visible = _locatorHudVisible; if (_locatorHudVisible) _locatorHud.Text = _locator.CurrentDisplay; return; } if (e is InputEventKey copy && copy.Pressed && !copy.Echo && copy.Keycode == Key.F3) { DisplayServer.ClipboardSet(_locator.CurrentDisplay); _locatorHud.Text = _locator.CurrentDisplay + " ยท copied"; return; } if (e is InputEventKey debugKey && debugKey.Pressed && !debugKey.Echo && debugKey.Keycode == Key.F4) { ToggleDebugSceneLauncher(); GetViewport().SetInputAsHandled(); return; } if (e is InputEventKey diagnosticKey && diagnosticKey.Keycode == Key.F6) { if (diagnosticKey.Pressed && !diagnosticKey.Echo) CaptureStallDiagnostic(); GetViewport().SetInputAsHandled(); return; } if (_debugSceneLauncher?.Visible == true) { if (e is InputEventKey escape && escape.Pressed && !escape.Echo && escape.Keycode == Key.Escape) { _debugSceneLauncher.Hide(); GetViewport().SetInputAsHandled(); } return; } if (e is InputEventMouseMotion motion) { var p = ToNativeScreen(motion.Position); _vm.UpdatePointer(p.X, p.Y); return; } if (e is InputEventMouseButton wheel && wheel.Pressed && (wheel.ButtonIndex == MouseButton.WheelUp || wheel.ButtonIndex == MouseButton.WheelDown)) { // WM_MOUSEWHEEL supplies signed multiples of WHEEL_DELTA (120). AGE accumulates that // value until op 0x10d reads and clears it; HISTORY currently uses only its sign. int direction = wheel.ButtonIndex == MouseButton.WheelUp ? 1 : -1; int steps = System.Math.Max(1, (int)System.Math.Round(wheel.Factor)); _vm.QueueMouseWheelDelta(direction * 120 * steps); GetViewport().SetInputAsHandled(); return; } if (e is InputEventMouseButton mb && (mb.ButtonIndex == MouseButton.Left || mb.ButtonIndex == MouseButton.Right)) { var p = ToNativeScreen(mb.Position); bool advPageSuspended = _host.IsAdvPagePresentationSuspended; 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); int action = _vm.UpdatePhysicalMouseButtonState(physicalButton, mb.Pressed); if (mb.Pressed && _host.IsModalMovieWaiting) { _host.SignalInput(); GetViewport().SetInputAsHandled(); return; } // Native blocking effect services poll logical action 4 before ADV hotspot dispatch. // Consume the trigger here so a retained hotspot under the transition cannot steal it. if (mb.Pressed && action == 4 && _host.IsTransitionWaiting) { _host.SignalInput(); GetViewport().SetInputAsHandled(); return; } // 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.Pressed && action >= 0 && _vm.TryActivateInputActions(1 << action)) { GetViewport().SetInputAsHandled(); return; } if (mb.ButtonIndex == MouseButton.Left && mb.Pressed && _vm.TryActivatePointer(p.X, p.Y)) { GetViewport().SetInputAsHandled(); return; } // Modal callback scripts return through their own bytecode. Signaling the enclosing ADV wait // here would also advance the restored dialogue page after HISTORY/HIDEWIN exits. if (mb.ButtonIndex == MouseButton.Left && mb.Pressed && !advPageSuspended && !rawInputCallbackActive) _host.SignalInput(); return; } if (e is InputEventKey gameplayKey && !gameplayKey.Echo && Win32VirtualKeyTranslator.TryTranslate(gameplayKey, out int virtualKey)) { int action = _vm.UpdateKeyboardVirtualKeyState(virtualKey, gameplayKey.Pressed); if (gameplayKey.Pressed && action == 4 && _host.IsTransitionWaiting) { _host.SignalInput(); GetViewport().SetInputAsHandled(); } else if (gameplayKey.Pressed && action >= 0 && _vm.TryActivateInputActions(1 << action)) { GetViewport().SetInputAsHandled(); } else if (gameplayKey.Pressed && IsAdvanceAction(action)) { if (_host.IsModalMovieWaiting) { _host.SignalInput(); GetViewport().SetInputAsHandled(); } else if (!_host.IsAdvPagePresentationSuspended && !_vm.IsRawInputCallbackActive) _host.SignalInput(); } return; } if (e is InputEventJoypadButton joyButton) { int actionMask = _vm.UpdateJoystickButtonState((int)joyButton.ButtonIndex, joyButton.Pressed); if (joyButton.Pressed && (actionMask & (1 << 4)) != 0 && _host.IsTransitionWaiting) { _host.SignalInput(); GetViewport().SetInputAsHandled(); } else if (joyButton.Pressed && _vm.TryActivateInputActions(actionMask)) { GetViewport().SetInputAsHandled(); } else 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 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() { if (_debugSceneLauncher == null) return; if (_debugSceneLauncher.Visible) { _debugSceneLauncher.Hide(); return; } if (!TryGetTitleDebugFrame(out var frame, out string reason)) { _status.Text = reason; GD.Print($"[debug-launcher] unavailable: {reason}"); return; } _debugSceneLauncher.Open(_debugSceneEntries, string.Join(" > ", frame.CallStack)); } private void LaunchDebugScene(DebugSceneEntry entry) { if (_debugSceneLauncher == null || _scripts == null) return; if (!TryGetTitleDebugFrame(out var frame, out string reason)) { _debugSceneLauncher.SetStatus(reason); return; } if (!entry.Launchable || _scripts.GetById(entry.PackedId) == null) { _debugSceneLauncher.SetStatus("The selected packed script could not be parsed; no state was changed."); return; } var coordinatorWrites = new Dictionary { [0] = 1, [0xaba5c] = -1, [0x62ccf] = 0, [0x699] = entry.PackedId, }; if (!_vm.TryRequestDebugFrameReturn(frame.FrameId, coordinatorWrites)) { _debugSceneLauncher.SetStatus("TITLE changed frames before launch; reopen the launcher and try again."); return; } _timeline?.Event("debug-scene-launch-request", new() { ["script"] = entry.Name, ["packed_id"] = entry.PackedId, }); GD.Print($"[debug-launcher] SYSTEM4 dispatch requested: {entry.Name} (0x{entry.PackedId:x8})"); _debugSceneLauncher.Hide(); // ADV waits need an explicit wake; TITLE's actual menu is a 1 ms sleep/poll loop and will consume // the request at its next opcode boundary without leaving a stale input signal for the child scene. if (_host.IsWaiting) _host.SignalInput(); } private bool TryGetTitleDebugFrame(out DebugFrameSnapshot frame, out string reason) { frame = _vm.DebugFrame!; if (_done || frame == null) { reason = "Available only while TITLE is the active SYSTEM4 child."; return false; } if (frame.CallStack.Count != 2 || !frame.CallStack[0].Equals("SYSTEM4.BIN", System.StringComparison.OrdinalIgnoreCase) || !frame.CallStack[1].Equals("TITLE.BIN", System.StringComparison.OrdinalIgnoreCase) || !frame.CurrentScript.Equals("TITLE.BIN", System.StringComparison.OrdinalIgnoreCase)) { reason = "Refused: the active stack is not SYSTEM4 > TITLE."; return false; } reason = ""; return true; } private (int X, int Y) ToNativeScreen(Vector2 position) { // Godot reports input in viewport coordinates after content scaling. This ratio is therefore // normally identity, while remaining correct for any viewport expansion policy. Vector2 size = GetViewport().GetVisibleRect().Size; if (size.X <= 0 || size.Y <= 0) return (0, 0); return ((int)System.Math.Floor(position.X * _screenWidth / size.X), (int)System.Math.Floor(position.Y * _screenHeight / size.Y)); } public void SetAgeCursor(byte[] rgba, int width, int height, int hotspotX, int hotspotY) { var image = Image.CreateFromData(width, height, false, Image.Format.Rgba8, rgba); _ageCursorTexture = ImageTexture.CreateFromImage(image); Input.SetCustomMouseCursor(_ageCursorTexture, Input.CursorShape.Arrow, new Vector2(hotspotX, hotspotY)); } public void ClearAgeCursor() { Input.SetCustomMouseCursor(null, Input.CursorShape.Arrow); _ageCursorTexture = null; } public void ShowAgeDiagnostic(string text, string caption) { try { OS.Alert(text, caption); } catch (System.Exception error) { GD.PushError($"[diagnostic] native alert failed: {error.Message}"); } finally { _host?.CompleteDiagnosticMessage(); } } public void ShowAgeFullwidthTextEditor(string initialText) { if (_fullwidthTextEditor != null) { GD.PushWarning("[inputname] replaced an already-open full-width text editor"); _fullwidthTextEditor.QueueFree(); } var editor = new FullwidthTextEditorDialog(); _fullwidthTextEditor = editor; editor.EditCompleted += (accepted, text) => { if (_fullwidthTextEditor == editor) _fullwidthTextEditor = null; _host?.CompleteFullwidthTextEdit(accepted, text); editor.QueueFree(); }; AddChild(editor); editor.Open(initialText); } }