Split Main input into partial class
This commit is contained in:
@@ -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-<SCENE>.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
|
||||
|
||||
@@ -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.
|
||||
|
||||
309
godot/Main.Input.cs
Normal file
309
godot/Main.Input.cs
Normal file
@@ -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<DebugSceneEntry> _debugSceneEntries = System.Array.Empty<DebugSceneEntry>();
|
||||
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<int, long>
|
||||
{
|
||||
[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);
|
||||
}
|
||||
|
||||
}
|
||||
299
godot/Main.cs
299
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<DebugSceneEntry> _debugSceneEntries = System.Array.Empty<DebugSceneEntry>();
|
||||
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<int, long>
|
||||
{
|
||||
[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;
|
||||
|
||||
Reference in New Issue
Block a user