diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index 9eaa7f4..7be890a 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -141,7 +141,9 @@ paths: `godot/Main.SelfTest.cs` owns the synthetic threaded/headless regression `godot/Main.Audio.cs` owns BGM, voice, sound-effect, mixer-routing/persistence, and audio-bus control, and `godot/Main.Movie.cs` owns decoder staging, movie frame/audio publication, completion, and teardown. `godot/Main.Compositor.cs` owns retained/GPU/software composition state, texture resolution and caching, -surface-transition drawing, raster helpers, and compositor decision logging. +surface-transition drawing, raster helpers, and compositor decision logging. `godot/Main.Input.cs` owns +Godot input routing, locator/debug hotkeys, debug-scene dispatch, cursor control, native alerts, and +full-width text entry. The disposable `build/page-map-.jsonl` files are produced by editor/development Godot runs and map runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index 183c7d1..e5333d4 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -559,13 +559,15 @@ do not mix mechanical moves with semantic changes. stage, battle, card, routine, gallery, and general table implementations plus tests into importable modules. - **Progress (2026-08-02):** the first four bounded splits moved `Main`'s synthetic scene builder/threaded + **Progress (2026-08-02):** five bounded splits completed the planned `Main` decomposition: its synthetic + scene builder/threaded self-test harness into `godot/Main.SelfTest.cs`, then its BGM, voice, sound-effect, mixer, and bus-control surface into `godot/Main.Audio.cs`, then decoder staging, movie frame/audio publication, completion, and teardown into `godot/Main.Movie.cs`, then retained GPU/software composition, texture caching, transitions, - raster helpers, and decision logging into `godot/Main.Compositor.cs`. The partial class retains the same - node type, fields, signatures, execution order, and call sites; runtime validation, including all 590 - engine tests and the Godot threaded self-test, remains green after each move. + raster helpers, and decision logging into `godot/Main.Compositor.cs`, and finally input routing, locator/debug + hotkeys, debug-scene dispatch, cursor/dialog handling, and full-width text entry into `godot/Main.Input.cs`. + The partial class retains the same node type, fields, signatures, execution order, and call sites; runtime + validation, including all 590 engine tests and the Godot threaded self-test, remains green after each move. **Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after @@ -937,8 +939,8 @@ layer's rendering diverges from ADV; save layout. ## 8. Immediate next step Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed -by the tracked launcher and layered validation driver. With the embedded `Main` self-test, audio, movie, and -retained compositor surfaces isolated, move input/cursor/debug-launcher handling next, then continue one -existing domain at a time while preserving public types, commands, and generated output. +by the tracked launcher and layered validation driver. With the planned `Main` domains isolated, begin +`GodotAdvHost` with its ADV-text surface, then continue one existing domain at a time while preserving public +types, commands, and generated output. Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does not replace Phase B gameplay validation or the open cross-platform gates. diff --git a/godot/Main.Input.cs b/godot/Main.Input.cs new file mode 100644 index 0000000..6f6f07f --- /dev/null +++ b/godot/Main.Input.cs @@ -0,0 +1,309 @@ +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); + } + +} diff --git a/godot/Main.cs b/godot/Main.cs index e852c24..41df6a2 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -25,20 +25,13 @@ public partial class Main : Godot.Control private WindowLaunchOptions _windowOptions; private Sys4AssetCatalog _catalog = null!; private IAssetStore _assetStore = null!; - private ImageTexture? _ageCursorTexture; private Label _status = null!; - private Label _locatorHud = null!; private VirtualMachine _vm = null!; private GodotAdvHost _host = null!; private IDisposable? _glyphRasterizerOwner; - private FullwidthTextEditorDialog? _fullwidthTextEditor; private Sys4ScriptProvider? _scripts; - private DebugSceneLauncher? _debugSceneLauncher; - private IReadOnlyList _debugSceneEntries = System.Array.Empty(); private readonly Age.Engine.Hosting.FrameClock _clock = new(); private GodotTraceSink _trace = null!; - private PageLocatorState _locator = null!; - private bool _locatorHudVisible; private Age.Engine.Diagnostics.HistogramTraceSink? _hist; // --trace-histogram: profile the real run private string? _histFile; private Age.Engine.Model.OpcodeTable? _table; @@ -577,162 +570,6 @@ public partial class Main : Godot.Control finally { perf?.EndFrame(); } } - // _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 CaptureStallDiagnostic(GodotTraceSnapshot? traceOverride = null, string snapshotKind = "stall") { @@ -829,142 +666,6 @@ public partial class Main : Godot.Control } } - 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); - } - public override void _ExitTree() { bool vmStopped = true;