Add debug foundation

This commit is contained in:
2026-04-21 21:16:30 +02:00
parent 693f31dd50
commit cc51f4a6e8
22 changed files with 1246 additions and 12 deletions

View File

@@ -0,0 +1,284 @@
#nullable enable
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Godot;
using SideScrollerGame.Debug.Commands;
namespace SideScrollerGame.Debug;
public partial class DebugSandboxController : Control
{
public override void _Ready()
{
ProcessMode = ProcessModeEnum.Always;
m_CommandNode = GetNodeOrNull<DebugCommandNode>("/root/GameRoot/DebugCommandNode");
if (m_CommandNode is null)
{
GD.PushError("Debug sandbox needs /root/GameRoot/DebugCommandNode.");
return;
}
BindSceneNodes();
BindCommandService();
RefreshLabels();
if (ShouldRunFoundationSmoke())
{
_ = RunFoundationSmokeAsync();
}
}
public override void _UnhandledInput(InputEvent @event)
{
if (m_CommandNode is null)
{
return;
}
if (@event.IsActionPressed("debug_overlay"))
{
Execute(DebugCommandId.ToggleOverlay);
}
else if (@event.IsActionPressed("pause_game") || @event.IsActionPressed("debug_pause"))
{
Execute(DebugCommandId.TogglePause);
}
else if (@event.IsActionPressed("debug_frame_step"))
{
Execute(DebugCommandId.FrameStep);
}
else if (@event.IsActionPressed("debug_time_slower"))
{
Execute(DebugCommandId.SetTimeScale, NextTimeScale(-1));
}
else if (@event.IsActionPressed("debug_time_faster"))
{
Execute(DebugCommandId.SetTimeScale, NextTimeScale(1));
}
else if (@event.IsActionPressed("debug_spawn_actor"))
{
Execute(DebugCommandId.SpawnActor, "enemy.serial");
}
else if (@event.IsActionPressed("debug_jump_marker"))
{
Execute(DebugCommandId.JumpToMarker, "cluster.opening");
}
else if (@event.IsActionPressed("debug_toggle_invulnerability"))
{
Execute(DebugCommandId.ToggleInvulnerability);
}
else if (@event.IsActionPressed("debug_toggle_infinite_special_ammo"))
{
Execute(DebugCommandId.ToggleInfiniteSpecialAmmo);
}
else if (@event.IsActionPressed("debug_toggle_no_enemy_fire"))
{
Execute(DebugCommandId.ToggleNoEnemyFire);
}
else if (@event.IsActionPressed("debug_toggle_collision_shapes"))
{
Execute(DebugCommandId.ToggleCollisionShapes);
}
else if (@event.IsActionPressed("debug_toggle_gameplay_bounds"))
{
Execute(DebugCommandId.ToggleGameplayBounds);
}
else if (@event.IsActionPressed("quick_restart"))
{
Execute(DebugCommandId.RestartMission);
}
}
private void BindSceneNodes()
{
m_MarkerLabel = GetNodeOrNull<Label>("Main/Playfield/PlayfieldContent/MarkerLabel");
m_SpawnedActors = GetNodeOrNull<VBoxContainer>("Main/Playfield/PlayfieldContent/SpawnedActors");
m_LogLabel = GetNodeOrNull<Label>("Main/LogScroll/LogLabel");
m_StateLabel = GetNodeOrNull<Label>("Main/Playfield/PlayfieldContent/StateLabel");
DebugPanelController? panel = GetNodeOrNull<DebugPanelController>("Main/DebugPanel");
if (panel is not null && m_CommandNode is not null)
{
panel.Bind(m_CommandNode);
}
}
private void BindCommandService()
{
if (m_CommandNode is null)
{
return;
}
DebugCommandService service = m_CommandNode.Service;
service.RegisterSpawnHandler(SpawnActor);
service.RegisterTimelineJumpHandler(JumpToMarker);
service.RegisterRestartHandler(RestartSandbox);
service.RegisterReloadHandler(() => DebugCommandResult.Success(DebugCommandId.ReloadScene, "Reload scene requested"));
service.CommandExecuted += HandleCommandExecuted;
service.StateChanged += _ => RefreshLabels();
}
private DebugCommandResult SpawnActor(string actorId)
{
Label label = new()
{
Text = $"{m_SpawnedActors?.GetChildCount() + 1 ?? 1}: {actorId}",
CustomMinimumSize = new Vector2(180.0f, 24.0f)
};
m_SpawnedActors?.AddChild(label);
return DebugCommandResult.Success(DebugCommandId.SpawnActor, $"Actor spawned: {actorId}", actorId);
}
private DebugCommandResult JumpToMarker(string markerId)
{
if (m_MarkerLabel is not null)
{
m_MarkerLabel.Text = $"Marker: {markerId}";
}
return DebugCommandResult.Success(DebugCommandId.JumpToMarker, $"Timeline marker: {markerId}", markerId);
}
private DebugCommandResult RestartSandbox()
{
if (m_SpawnedActors is not null)
{
foreach (Node child in m_SpawnedActors.GetChildren())
{
child.QueueFree();
}
}
if (m_MarkerLabel is not null)
{
m_MarkerLabel.Text = "Marker: none";
}
return DebugCommandResult.Success(DebugCommandId.RestartMission, "Sandbox restarted");
}
private void HandleCommandExecuted(DebugCommandResult result)
{
string suffix = string.IsNullOrWhiteSpace(result.Argument) ? string.Empty : $" {result.Argument}";
AppendLog($"Command executed: {result.CommandId}{suffix}");
if (!result.Succeeded)
{
AppendLog($"Command failed: {result.Message}");
return;
}
if (result.CommandId is DebugCommandId.SpawnActor or DebugCommandId.JumpToMarker)
{
AppendLog(result.Message);
}
}
private void RefreshLabels()
{
if (m_CommandNode is null)
{
return;
}
DebugRuntimeState state = m_CommandNode.Service.State;
if (m_StateLabel is not null)
{
m_StateLabel.Text = $"Difficulty: {state.ActiveDifficultyId} | Seed: {state.Seed} | Paused: {state.IsPaused} | Time: {state.TimeScale.ToString(CultureInfo.InvariantCulture)} | Spawns: {state.SpawnedActorCount}";
}
if (m_MarkerLabel is not null && string.IsNullOrWhiteSpace(state.CurrentMarkerId))
{
m_MarkerLabel.Text = "Marker: none";
}
}
private void Execute(DebugCommandId commandId, string? argument = null)
{
m_CommandNode?.Execute(commandId, argument);
}
private string NextTimeScale(int direction)
{
if (m_CommandNode is null)
{
return "1";
}
double current = m_CommandNode.Service.State.TimeScale;
int index = Array.IndexOf(s_TimeScales, current);
if (index < 0)
{
index = Array.IndexOf(s_TimeScales, 1.0);
}
int nextIndex = Math.Clamp(index + direction, 0, s_TimeScales.Length - 1);
return s_TimeScales[nextIndex].ToString(CultureInfo.InvariantCulture);
}
private async Task RunFoundationSmokeAsync()
{
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
AppendLog("Debug foundation smoke loaded");
bool succeeded = ExecuteAndRequire(DebugCommandId.Pause) && ExecuteAndRequire(DebugCommandId.SetTimeScale, "0.5") && ExecuteAndRequire(DebugCommandId.SetDifficulty, "difficulty.hard") && ExecuteAndRequire(DebugCommandId.SetSeed, m_CommandNode?.Service.State.Seed.ToString(CultureInfo.InvariantCulture)) && ExecuteAndRequire(DebugCommandId.SpawnActor, "enemy.serial") && ExecuteAndRequire(DebugCommandId.JumpToMarker, "cluster.opening") && ExecuteAndRequire(DebugCommandId.ToggleInvulnerability) && ExecuteAndRequire(DebugCommandId.ToggleInfiniteSpecialAmmo) && ExecuteAndRequire(DebugCommandId.ToggleNoEnemyFire) && ExecuteAndRequire(DebugCommandId.ToggleCollisionShapes) && ExecuteAndRequire(DebugCommandId.ToggleGameplayBounds) && ExecuteAndRequire(DebugCommandId.RestartMission) && ExecuteAndRequire(DebugCommandId.SpawnActor, "enemy.parallel") && VerifyFoundationSmokeState();
AppendLog(succeeded ? "Debug foundation smoke succeeded" : "Debug foundation smoke failed");
GetTree().Quit(succeeded ? 0 : 1);
}
private bool ExecuteAndRequire(DebugCommandId commandId, string? argument = null)
{
if (m_CommandNode is null)
{
return false;
}
DebugCommandResult result = m_CommandNode.Execute(commandId, argument);
if (result.Succeeded)
{
return true;
}
AppendLog(result.Message);
return false;
}
private bool VerifyFoundationSmokeState()
{
if (m_CommandNode is null)
{
return false;
}
DebugRuntimeState state = m_CommandNode.Service.State;
return state.IsPaused && Math.Abs(state.TimeScale - 0.5) < 0.001 && state.ActiveDifficultyId == "difficulty.hard" && state.LastSpawnedActorId == "enemy.parallel" && state.SpawnedActorCount == 1 && state.RestartMissionRequestCount == 1 && state.Invulnerable && state.InfiniteSpecialAmmo && state.NoEnemyFire && state.ShowCollisionShapes && state.ShowGameplayBounds;
}
private void AppendLog(string message)
{
GD.Print(message);
if (m_LogLabel is null)
{
return;
}
m_LogLabel.Text = string.IsNullOrWhiteSpace(m_LogLabel.Text) ? message : $"{m_LogLabel.Text}\n{message}";
}
private static bool ShouldRunFoundationSmoke()
{
return DisplayServer.GetName().Equals("headless", StringComparison.OrdinalIgnoreCase) && OS.GetCmdlineUserArgs().Any(argument => argument.Equals("--debug-script=foundation-smoke", StringComparison.OrdinalIgnoreCase));
}
private static readonly double[] s_TimeScales = [0.25, 0.5, 1.0, 2.0, 4.0];
private DebugCommandNode? m_CommandNode;
private Label? m_MarkerLabel;
private VBoxContainer? m_SpawnedActors;
private Label? m_LogLabel;
private Label? m_StateLabel;
}