Implement root reload and title debug launcher
This commit is contained in:
127
godot/DebugSceneLauncher.cs
Normal file
127
godot/DebugSceneLauncher.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Age.Engine.Diagnostics;
|
||||
using Godot;
|
||||
|
||||
/// <summary>Godot-only developer overlay. Runtime transition policy remains in Main/VirtualMachine.</summary>
|
||||
public partial class DebugSceneLauncher : PopupPanel
|
||||
{
|
||||
private readonly LineEdit _search = new() { PlaceholderText = "Name or exact packed id (0x...)" };
|
||||
private readonly OptionButton _category = new();
|
||||
private readonly ItemList _list = new() { SelectMode = ItemList.SelectModeEnum.Single };
|
||||
private readonly Label _details = new() { AutowrapMode = TextServer.AutowrapMode.WordSmart };
|
||||
private readonly Label _status = new() { AutowrapMode = TextServer.AutowrapMode.WordSmart };
|
||||
private readonly Button _launch = new() { Text = "Launch", Disabled = true };
|
||||
private IReadOnlyList<DebugSceneEntry> _all = Array.Empty<DebugSceneEntry>();
|
||||
private IReadOnlyList<DebugSceneEntry> _visible = Array.Empty<DebugSceneEntry>();
|
||||
private DebugSceneEntry? _selected;
|
||||
private string _currentContext = "";
|
||||
|
||||
public event Action<DebugSceneEntry>? LaunchRequested;
|
||||
|
||||
public DebugSceneLauncher()
|
||||
{
|
||||
Title = "AGE Debug Scene Launcher";
|
||||
Exclusive = true;
|
||||
|
||||
var margin = new MarginContainer();
|
||||
margin.AddThemeConstantOverride("margin_left", 14);
|
||||
margin.AddThemeConstantOverride("margin_top", 14);
|
||||
margin.AddThemeConstantOverride("margin_right", 14);
|
||||
margin.AddThemeConstantOverride("margin_bottom", 14);
|
||||
AddChild(margin);
|
||||
margin.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
|
||||
|
||||
var column = new VBoxContainer();
|
||||
margin.AddChild(column);
|
||||
|
||||
var heading = new Label { Text = "Launch a packed SYS4 script through SYSTEM4" };
|
||||
heading.AddThemeFontSizeOverride("font_size", 18);
|
||||
column.AddChild(heading);
|
||||
|
||||
var filters = new HBoxContainer();
|
||||
column.AddChild(filters);
|
||||
_category.AddItem("All");
|
||||
_category.AddItem("Scenario (SC)");
|
||||
_category.AddItem("Secondary / Event (SP)");
|
||||
_category.AddItem("Debug");
|
||||
_category.AddItem("Other / Expert");
|
||||
_category.CustomMinimumSize = new Vector2(190, 0);
|
||||
filters.AddChild(_category);
|
||||
_search.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
filters.AddChild(_search);
|
||||
|
||||
_list.CustomMinimumSize = new Vector2(0, 290);
|
||||
_list.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
|
||||
column.AddChild(_list);
|
||||
|
||||
_details.CustomMinimumSize = new Vector2(0, 76);
|
||||
column.AddChild(_details);
|
||||
_status.CustomMinimumSize = new Vector2(0, 34);
|
||||
column.AddChild(_status);
|
||||
|
||||
var actions = new HBoxContainer { Alignment = BoxContainer.AlignmentMode.End };
|
||||
column.AddChild(actions);
|
||||
var cancel = new Button { Text = "Cancel" };
|
||||
actions.AddChild(cancel);
|
||||
actions.AddChild(_launch);
|
||||
|
||||
_search.TextChanged += _ => Refresh();
|
||||
_category.ItemSelected += _ => Refresh();
|
||||
_list.ItemSelected += SelectEntry;
|
||||
_list.ItemActivated += SelectAndLaunch;
|
||||
cancel.Pressed += Hide;
|
||||
_launch.Pressed += RequestLaunch;
|
||||
}
|
||||
|
||||
public void Open(IReadOnlyList<DebugSceneEntry> entries, string currentContext)
|
||||
{
|
||||
_all = entries;
|
||||
_currentContext = currentContext;
|
||||
_status.Text = "";
|
||||
Refresh();
|
||||
PopupCentered(new Vector2I(700, 540));
|
||||
_search.GrabFocus();
|
||||
}
|
||||
|
||||
public void SetStatus(string message) => _status.Text = message;
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
var filter = (DebugScriptFilter)_category.Selected;
|
||||
_visible = DebugSceneCatalog.Filter(_all, filter, _search.Text);
|
||||
_list.Clear();
|
||||
foreach (var entry in _visible)
|
||||
_list.AddItem($"{entry.Name} 0x{entry.PackedId:x8}");
|
||||
_selected = null;
|
||||
_launch.Disabled = true;
|
||||
_details.Text = $"{_visible.Count} scripts shown. Current: {_currentContext}";
|
||||
}
|
||||
|
||||
private void SelectEntry(long index)
|
||||
{
|
||||
if (index < 0 || index >= _visible.Count) return;
|
||||
_selected = _visible[(int)index];
|
||||
_launch.Disabled = !_selected.Launchable;
|
||||
string guard = _selected.Launchable
|
||||
? "Launch returns TITLE to SYSTEM4, which performs the actual script dispatch. " +
|
||||
"Current live globals/profile state is retained; no story state is synthesized."
|
||||
: "Protected coordinator/root script; direct launch is disabled.";
|
||||
_details.Text =
|
||||
$"{_selected.Name} [{_selected.Kind}]\n" +
|
||||
$"packed=0x{_selected.PackedId:x8} ({_selected.PackedId}) " +
|
||||
$"pack={_selected.PackId} raw=0x{_selected.RawIndex:x} " +
|
||||
$"archive={_selected.Archive} size={_selected.Size:N0}\n{guard}";
|
||||
}
|
||||
|
||||
private void SelectAndLaunch(long index)
|
||||
{
|
||||
SelectEntry(index);
|
||||
RequestLaunch();
|
||||
}
|
||||
|
||||
private void RequestLaunch()
|
||||
{
|
||||
if (_selected is { Launchable: true } selected) LaunchRequested?.Invoke(selected);
|
||||
}
|
||||
}
|
||||
@@ -491,6 +491,44 @@ public sealed class GodotAdvHost : IHost
|
||||
_frameSignal.Set();
|
||||
}
|
||||
|
||||
public void ResetSceneContext()
|
||||
{
|
||||
// scene_context_init_reset releases ordinary surface/movie bindings but keeps decoded asset
|
||||
// caches and process-owned audio/configuration available to the reloaded SYSTEM4 root.
|
||||
ReleaseSurfaceRange(0, 1000);
|
||||
lock (_textLock)
|
||||
{
|
||||
_surfaceText.Clear();
|
||||
_surfaceResources.Clear();
|
||||
_historyText.Clear();
|
||||
_advText = "";
|
||||
_advTextX = 100;
|
||||
_advTextY = 47;
|
||||
_advTextForceComplete = false;
|
||||
_waitIndicators.Clear();
|
||||
_activeWaitLayout = 0;
|
||||
_waitIndicatorEnabled = false;
|
||||
}
|
||||
while (_gate.Wait(0)) { }
|
||||
_inputCallbackSignal.WaitOne(0);
|
||||
_messageSkipActive = false;
|
||||
_queuedSkippedVoice = null;
|
||||
System.Threading.Volatile.Write(ref _voiceBgmDuckControl, 0);
|
||||
_advPagePresentationSuspended = false;
|
||||
_modalMovieCancelled = false;
|
||||
_modalMovieWaiting = false;
|
||||
_foregroundGfx = null;
|
||||
IsWaiting = false;
|
||||
IsTransitionWaiting = false;
|
||||
IsSleeping = false;
|
||||
IsTextRevealing = false;
|
||||
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, -1);
|
||||
System.Threading.Interlocked.Exchange(ref _presentRequested, 1);
|
||||
_main.CallDeferred("CancelScheduledSoundEffectStarts");
|
||||
_main.CallDeferred("ClearPage");
|
||||
_timeline?.State("scene-context-reset", new());
|
||||
}
|
||||
|
||||
// Main thread, once per rendered frame: releases a VM thread parked in Sleep or a presentation/input wait.
|
||||
public void PulseFrame() => _frameSignal.Set();
|
||||
|
||||
|
||||
127
godot/Main.cs
127
godot/Main.cs
@@ -1,8 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Godot;
|
||||
using Age.Engine.Diagnostics;
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
@@ -45,6 +47,9 @@ public partial class Main : Godot.Control
|
||||
private readonly int[] _sfxGenerations = new int[10];
|
||||
private VirtualMachine _vm = null!;
|
||||
private GodotAdvHost _host = null!;
|
||||
private Sys4ScriptProvider? _scripts;
|
||||
private DebugSceneLauncher? _debugSceneLauncher;
|
||||
private IReadOnlyList<DebugSceneEntry> _debugSceneEntries = System.Array.Empty<DebugSceneEntry>();
|
||||
private readonly Age.Engine.Hosting.FrameClock _clock = new();
|
||||
private readonly System.Collections.Generic.Dictionary<long, MovieRuntime> _movies = new();
|
||||
private readonly System.Collections.Generic.HashSet<long> _movieFrameSeen = new();
|
||||
@@ -200,6 +205,7 @@ public partial class Main : Godot.Control
|
||||
Sys4ScriptProvider? scripts = null;
|
||||
if (_selftest) (script, provider) = BuildSelfTestScene(table);
|
||||
else { scripts = Sys4ScriptProvider.Load(table); script = scripts.RequireByName(scene + ".BIN"); provider = scripts; }
|
||||
_scripts = scripts;
|
||||
bool directSceneHarness = !_selftest
|
||||
&& !scene.Equals("SYSTEM4", System.StringComparison.OrdinalIgnoreCase);
|
||||
if (_timelineLogPath != null) _timeline = new GodotTimelineLog(_timelineLogPath);
|
||||
@@ -218,6 +224,13 @@ public partial class Main : Godot.Control
|
||||
if (histFile != null) { _hist = new Age.Engine.Diagnostics.HistogramTraceSink();
|
||||
sink = new Age.Engine.Diagnostics.CompositeTraceSink(_trace, _hist); }
|
||||
_vm = new VirtualMachine(script, table, _host, new VmOptions(MaxSteps: 20_000_000), provider, sink);
|
||||
if (scripts != null)
|
||||
{
|
||||
_debugSceneEntries = DebugSceneCatalog.Build(scripts.Catalog);
|
||||
_debugSceneLauncher = new DebugSceneLauncher();
|
||||
_debugSceneLauncher.LaunchRequested += LaunchDebugScene;
|
||||
AddChild(_debugSceneLauncher);
|
||||
}
|
||||
// SYSTEM4.BIN defines these nine shared ADV text layouts before dispatching any scene. The
|
||||
// single-scene harness starts after that prefix, so carry forward its exact script-owned state
|
||||
// alongside the inherited SO000/SO001 state below. Full Phase-B SYSTEM4 replay will replace this
|
||||
@@ -359,6 +372,21 @@ public partial class Main : Godot.Control
|
||||
_locatorHud.Text = _locator.CurrentDisplay + " · copied";
|
||||
return;
|
||||
}
|
||||
if (e is InputEventKey debugKey && debugKey.Pressed && !debugKey.Echo && debugKey.Keycode == Key.F4)
|
||||
{
|
||||
ToggleDebugSceneLauncher();
|
||||
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);
|
||||
@@ -435,6 +463,82 @@ 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)
|
||||
{
|
||||
Vector2 size = GetViewportRect().Size;
|
||||
@@ -973,6 +1077,14 @@ public partial class Main : Godot.Control
|
||||
GetTree().CreateTimer(realDelaySeconds).Timeout += StartIfCurrent;
|
||||
}
|
||||
|
||||
public void CancelScheduledSoundEffectStarts()
|
||||
{
|
||||
// Native scene_context_init_reset calls sfx_clear_scheduled_starts. Generation invalidation
|
||||
// cancels the timer callbacks without stopping active sounds or unloading their channel streams.
|
||||
for (int channel = 0; channel < _sfxGenerations.Length; channel++)
|
||||
_sfxGenerations[channel]++;
|
||||
}
|
||||
|
||||
public void ReleaseSoundEffect(int channel)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfx.Length) return;
|
||||
@@ -1068,8 +1180,19 @@ public partial class Main : Godot.Control
|
||||
var actual = _host.Captured.ConvertAll(c => c.Offset);
|
||||
bool ok = actual.Count == expected.Count;
|
||||
for (int i = 0; ok && i < actual.Count; i++) ok = actual[i] == expected[i];
|
||||
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling)");
|
||||
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}");
|
||||
var debugEntries = DebugSceneCatalog.Build(Sys4AssetCatalog.Load(Paths.Sys4Ini));
|
||||
bool launcherOk = debugEntries.Any(entry => entry.Name == "DEBUG.BIN" && entry.Launchable)
|
||||
&& debugEntries.Select(entry => entry.PackedId).Distinct().Count() == debugEntries.Count;
|
||||
var launcherSmoke = new DebugSceneLauncher();
|
||||
AddChild(launcherSmoke);
|
||||
launcherSmoke.Open(debugEntries, "SYSTEM4.BIN > TITLE.BIN");
|
||||
launcherSmoke.Hide();
|
||||
launcherSmoke.QueueFree();
|
||||
ok &= launcherOk;
|
||||
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
||||
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts)");
|
||||
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
|
||||
$"debug-launcher={launcherOk}");
|
||||
GetTree().Quit(ok ? 0 : 1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user