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.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.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,
|
`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
|
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
|
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
|
stage, battle, card, routine, gallery, and general table implementations plus tests into importable
|
||||||
modules.
|
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
|
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
|
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,
|
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
|
raster helpers, and decision logging into `godot/Main.Compositor.cs`, and finally input routing, locator/debug
|
||||||
node type, fields, signatures, execution order, and call sites; runtime validation, including all 590
|
hotkeys, debug-scene dispatch, cursor/dialog handling, and full-width text entry into `godot/Main.Input.cs`.
|
||||||
engine tests and the Godot threaded self-test, remains green after each move.
|
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
|
**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
|
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
|
## 8. Immediate next step
|
||||||
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
|
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
|
by the tracked launcher and layered validation driver. With the planned `Main` domains isolated, begin
|
||||||
retained compositor surfaces isolated, move input/cursor/debug-launcher handling next, then continue one
|
`GodotAdvHost` with its ADV-text surface, then continue one existing domain at a time while preserving public
|
||||||
existing domain at a time while preserving public types, commands, and generated output.
|
types, commands, and generated output.
|
||||||
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
|
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.
|
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 WindowLaunchOptions _windowOptions;
|
||||||
private Sys4AssetCatalog _catalog = null!;
|
private Sys4AssetCatalog _catalog = null!;
|
||||||
private IAssetStore _assetStore = null!;
|
private IAssetStore _assetStore = null!;
|
||||||
private ImageTexture? _ageCursorTexture;
|
|
||||||
private Label _status = null!;
|
private Label _status = null!;
|
||||||
private Label _locatorHud = null!;
|
|
||||||
private VirtualMachine _vm = null!;
|
private VirtualMachine _vm = null!;
|
||||||
private GodotAdvHost _host = null!;
|
private GodotAdvHost _host = null!;
|
||||||
private IDisposable? _glyphRasterizerOwner;
|
private IDisposable? _glyphRasterizerOwner;
|
||||||
private FullwidthTextEditorDialog? _fullwidthTextEditor;
|
|
||||||
private Sys4ScriptProvider? _scripts;
|
private Sys4ScriptProvider? _scripts;
|
||||||
private DebugSceneLauncher? _debugSceneLauncher;
|
|
||||||
private IReadOnlyList<DebugSceneEntry> _debugSceneEntries = System.Array.Empty<DebugSceneEntry>();
|
|
||||||
private readonly Age.Engine.Hosting.FrameClock _clock = new();
|
private readonly Age.Engine.Hosting.FrameClock _clock = new();
|
||||||
private GodotTraceSink _trace = null!;
|
private GodotTraceSink _trace = null!;
|
||||||
private PageLocatorState _locator = null!;
|
|
||||||
private bool _locatorHudVisible;
|
|
||||||
private Age.Engine.Diagnostics.HistogramTraceSink? _hist; // --trace-histogram: profile the real run
|
private Age.Engine.Diagnostics.HistogramTraceSink? _hist; // --trace-histogram: profile the real run
|
||||||
private string? _histFile;
|
private string? _histFile;
|
||||||
private Age.Engine.Model.OpcodeTable? _table;
|
private Age.Engine.Model.OpcodeTable? _table;
|
||||||
@@ -577,162 +570,6 @@ public partial class Main : Godot.Control
|
|||||||
finally { perf?.EndFrame(); }
|
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,
|
private void CaptureStallDiagnostic(GodotTraceSnapshot? traceOverride = null,
|
||||||
string snapshotKind = "stall")
|
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()
|
public override void _ExitTree()
|
||||||
{
|
{
|
||||||
bool vmStopped = true;
|
bool vmStopped = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user