284 lines
9.8 KiB
C#
284 lines
9.8 KiB
C#
#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;
|
|
} |