#nullable enable using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using SideScrollerGame.Content; namespace SideScrollerGame.Debug.Commands; public sealed class DebugCommandService { public DebugCommandService(ContentRegistry registry, int seed) { m_Registry = registry; State = new DebugRuntimeState(FindDefaultDifficultyId(registry), seed); } public DebugCommandResult Execute(DebugCommandId commandId, string? argument = null) { DebugCommandResult result = commandId switch { DebugCommandId.ToggleOverlay => ToggleOverlay(), DebugCommandId.Pause => SetPaused(commandId, true), DebugCommandId.Resume => SetPaused(commandId, false), DebugCommandId.TogglePause => SetPaused(commandId, !State.IsPaused), DebugCommandId.FrameStep => FrameStep(), DebugCommandId.SetTimeScale => SetTimeScale(argument), DebugCommandId.ReloadScene => ReloadScene(), DebugCommandId.RestartMission => RestartMission(), DebugCommandId.SetDifficulty => SetDifficulty(argument), DebugCommandId.SetSeed => SetSeed(argument), DebugCommandId.SpawnActor => SpawnActor(argument), DebugCommandId.JumpToMarker => JumpToMarker(argument), DebugCommandId.ToggleCollisionShapes => ToggleFlag(commandId, nameof(State.ShowCollisionShapes)), DebugCommandId.ToggleGameplayBounds => ToggleFlag(commandId, nameof(State.ShowGameplayBounds)), DebugCommandId.ToggleInvulnerability => ToggleFlag(commandId, nameof(State.Invulnerable)), DebugCommandId.ToggleInfiniteSpecialAmmo => ToggleFlag(commandId, nameof(State.InfiniteSpecialAmmo)), DebugCommandId.ToggleNoEnemyFire => ToggleFlag(commandId, nameof(State.NoEnemyFire)), _ => ExecuteRegisteredCommand(commandId, argument) }; CommandExecuted?.Invoke(result); return result; } public void RegisterSpawnHandler(Func handler) { m_SpawnHandler = handler; } public void RegisterTimelineJumpHandler(Func handler) { m_TimelineJumpHandler = handler; } public void RegisterReloadHandler(Func handler) { m_ReloadHandler = handler; } public void RegisterRestartHandler(Func handler) { m_RestartHandler = handler; } public void RegisterCommandHandler(DebugCommandId commandId, Func handler) { m_CommandHandlers[commandId] = handler; } public void ClearCommandHandler(DebugCommandId commandId) { m_CommandHandlers.Remove(commandId); } public DebugRuntimeState State { get; } public event Action? StateChanged; public event Action? CommandExecuted; private DebugCommandResult ToggleOverlay() { State.OverlayVisible = !State.OverlayVisible; NotifyStateChanged(); return DebugCommandResult.Success(DebugCommandId.ToggleOverlay, $"Overlay visible: {State.OverlayVisible}"); } private DebugCommandResult SetPaused(DebugCommandId commandId, bool isPaused) { State.IsPaused = isPaused; NotifyStateChanged(); return DebugCommandResult.Success(commandId, State.IsPaused ? "Paused" : "Resumed"); } private DebugCommandResult FrameStep() { State.IsPaused = true; State.FrameStepRequestCount++; NotifyStateChanged(); return DebugCommandResult.Success(DebugCommandId.FrameStep, "Frame step requested"); } private DebugCommandResult SetTimeScale(string? argument) { if (!double.TryParse(argument, NumberStyles.Float, CultureInfo.InvariantCulture, out double timeScale) || !s_SupportedTimeScales.Contains(timeScale)) { return DebugCommandResult.Failure(DebugCommandId.SetTimeScale, $"Unsupported time scale '{argument}'.", argument); } State.TimeScale = timeScale; NotifyStateChanged(); return DebugCommandResult.Success(DebugCommandId.SetTimeScale, $"Time scale set to {timeScale.ToString(CultureInfo.InvariantCulture)}", argument); } private DebugCommandResult ReloadScene() { State.ReloadSceneRequestCount++; DebugCommandResult result = m_ReloadHandler?.Invoke() ?? DebugCommandResult.Success(DebugCommandId.ReloadScene, "Reload scene requested"); NotifyStateChanged(); return result.Succeeded ? DebugCommandResult.Success(DebugCommandId.ReloadScene, result.Message) : result; } private DebugCommandResult RestartMission() { State.RestartMissionRequestCount++; State.CurrentMarkerId = string.Empty; State.LastSpawnedActorId = string.Empty; State.SpawnedActorCount = 0; DebugCommandResult result = m_RestartHandler?.Invoke() ?? DebugCommandResult.Success(DebugCommandId.RestartMission, "Restart mission requested"); NotifyStateChanged(); return result.Succeeded ? DebugCommandResult.Success(DebugCommandId.RestartMission, result.Message) : result; } private DebugCommandResult SetDifficulty(string? argument) { string difficultyId = RequireArgument(argument); if (difficultyId.Length == 0) { return DebugCommandResult.Failure(DebugCommandId.SetDifficulty, "Missing difficulty id.", argument); } if (!m_Registry.Difficulties.ContainsKey(difficultyId)) { return DebugCommandResult.Failure(DebugCommandId.SetDifficulty, $"Unknown difficulty '{difficultyId}'.", difficultyId); } State.ActiveDifficultyId = difficultyId; NotifyStateChanged(); return DebugCommandResult.Success(DebugCommandId.SetDifficulty, $"Difficulty set to {difficultyId}", difficultyId); } private DebugCommandResult SetSeed(string? argument) { if (!int.TryParse(argument, NumberStyles.Integer, CultureInfo.InvariantCulture, out int seed)) { return DebugCommandResult.Failure(DebugCommandId.SetSeed, $"Invalid seed '{argument}'.", argument); } State.Seed = seed; NotifyStateChanged(); return DebugCommandResult.Success(DebugCommandId.SetSeed, $"Seed set to {seed}", argument); } private DebugCommandResult SpawnActor(string? argument) { string actorId = RequireArgument(argument); if (actorId.Length == 0) { return DebugCommandResult.Failure(DebugCommandId.SpawnActor, "Missing actor id.", argument); } if (!m_Registry.EnemyTypes.ContainsKey(actorId)) { return DebugCommandResult.Failure(DebugCommandId.SpawnActor, $"Unknown actor '{actorId}'.", actorId); } DebugCommandResult result = m_SpawnHandler?.Invoke(actorId) ?? DebugCommandResult.Success(DebugCommandId.SpawnActor, $"Spawn actor requested: {actorId}", actorId); if (!result.Succeeded) { return result; } State.LastSpawnedActorId = actorId; State.SpawnedActorCount++; NotifyStateChanged(); return DebugCommandResult.Success(DebugCommandId.SpawnActor, result.Message, actorId); } private DebugCommandResult JumpToMarker(string? argument) { string markerId = RequireArgument(argument); if (markerId.Length == 0) { return DebugCommandResult.Failure(DebugCommandId.JumpToMarker, "Missing marker id.", argument); } if (!HasMissionMarker(markerId)) { return DebugCommandResult.Failure(DebugCommandId.JumpToMarker, $"Unknown marker '{markerId}'.", markerId); } DebugCommandResult result = m_TimelineJumpHandler?.Invoke(markerId) ?? DebugCommandResult.Success(DebugCommandId.JumpToMarker, $"Jump marker requested: {markerId}", markerId); if (!result.Succeeded) { return result; } State.CurrentMarkerId = markerId; NotifyStateChanged(); return DebugCommandResult.Success(DebugCommandId.JumpToMarker, result.Message, markerId); } private DebugCommandResult ToggleFlag(DebugCommandId commandId, string propertyName) { string message = propertyName switch { nameof(State.ShowCollisionShapes) => ToggleShowCollisionShapes(), nameof(State.ShowGameplayBounds) => ToggleShowGameplayBounds(), nameof(State.Invulnerable) => ToggleInvulnerable(), nameof(State.InfiniteSpecialAmmo) => ToggleInfiniteSpecialAmmo(), nameof(State.NoEnemyFire) => ToggleNoEnemyFire(), _ => $"Unsupported flag '{propertyName}'." }; NotifyStateChanged(); return DebugCommandResult.Success(commandId, message); } private DebugCommandResult ExecuteRegisteredCommand(DebugCommandId commandId, string? argument) { return m_CommandHandlers.TryGetValue(commandId, out Func? handler) ? handler(argument) : DebugCommandResult.Failure(commandId, $"Unsupported debug command '{commandId}'.", argument); } private string ToggleShowCollisionShapes() { State.ShowCollisionShapes = !State.ShowCollisionShapes; return $"Collision shapes: {State.ShowCollisionShapes}"; } private string ToggleShowGameplayBounds() { State.ShowGameplayBounds = !State.ShowGameplayBounds; return $"Gameplay bounds: {State.ShowGameplayBounds}"; } private string ToggleInvulnerable() { State.Invulnerable = !State.Invulnerable; return $"Invulnerable: {State.Invulnerable}"; } private string ToggleInfiniteSpecialAmmo() { State.InfiniteSpecialAmmo = !State.InfiniteSpecialAmmo; return $"Infinite special ammo: {State.InfiniteSpecialAmmo}"; } private string ToggleNoEnemyFire() { State.NoEnemyFire = !State.NoEnemyFire; return $"No enemy fire: {State.NoEnemyFire}"; } private bool HasMissionMarker(string markerId) { return m_Registry.MissionDefinitions.Any(mission => mission.TimelineMarkers.Contains(markerId)); } private void NotifyStateChanged() { StateChanged?.Invoke(State); } private static string RequireArgument(string? argument) { return string.IsNullOrWhiteSpace(argument) ? string.Empty : argument.Trim(); } private static string FindDefaultDifficultyId(ContentRegistry registry) { if (registry.TryGetMission("mission.test", out var mission) && mission is not null && registry.Difficulties.ContainsKey(mission.DefaultDifficultyId)) { return mission.DefaultDifficultyId; } return registry.DifficultyDefinitions.FirstOrDefault()?.Id ?? string.Empty; } private static readonly HashSet s_SupportedTimeScales = [0.25, 0.5, 1.0, 2.0, 4.0]; private readonly ContentRegistry m_Registry; private readonly Dictionary> m_CommandHandlers = []; private Func? m_SpawnHandler; private Func? m_TimelineJumpHandler; private Func? m_ReloadHandler; private Func? m_RestartHandler; }