diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 8c2b38345..a47577d38 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -106,6 +106,9 @@ public class ActionGameManager : MonoBehaviour } public static Galaxy CurrentGalaxy; + // Whether a run is live: a failed start and a finished run both clear the galaxy, and the scene's update paths (this + // manager's, the renderer's) step and draw nothing without one. + public static bool RunLive => CurrentGalaxy != null; // The scenario a new run starts from: MainMenu.Launch sets it, and StartGame's new-run branch consumes it once. public static Scenario PendingScenario; @@ -854,21 +857,12 @@ public class ActionGameManager : MonoBehaviour var scenario = PendingScenario; PendingScenario = null; var failures = new List(); - RunStart.Staged staged = null; - try + var staged = RunStart.Launch(() => RunSave.Replace(CultCache, () => { - staged = RunSave.Replace(CultCache, () => - { - RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, Settings.PlanetSettings, CurrentGalaxy, scenario); - PopulateLevel(CurrentGalaxy.Entrance); - return RunStart.Stage(ItemManager, Zone, scenario, Settings.StartingHullName, Settings.TutorialGenerationSettings, failures); - }); - } - catch (Exception exception) - { - Debug.LogException(exception); - failures.Add(exception.Message); - } + RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, Settings.PlanetSettings, CurrentGalaxy, scenario); + PopulateLevel(CurrentGalaxy.Entrance); + return RunStart.Stage(ItemManager, Zone, scenario, Settings.StartingHullName, Settings.TutorialGenerationSettings, failures); + }), failures, Debug.LogException); if (staged == null) { @@ -1348,6 +1342,7 @@ public class ActionGameManager : MonoBehaviour void Update() { + if (!RunLive) return; if(!_paused) { _time += Time.deltaTime; diff --git a/Assets/Scripts/ServerShared/RunStart.cs b/Assets/Scripts/ServerShared/RunStart.cs index cadd66bcc..4e6808859 100644 --- a/Assets/Scripts/ServerShared/RunStart.cs +++ b/Assets/Scripts/ServerShared/RunStart.cs @@ -30,6 +30,21 @@ public static class RunStart itemManager, zoneSettings, planetSettings, galaxy, galaxy.Entrance, galaxy.IsPrelude, scenario.Ambient); } + // Runs a start and answers whether it produced a run: null, with the reason in `failures`, when `start` throws or + // returns nothing. The caller shows the failure and clears its run; nothing past a null may run the game. + public static Staged Launch(Func start, List failures, Action log = null) + { + Staged staged = null; + try { staged = start(); } + catch (Exception exception) + { + log?.Invoke(exception); + failures.Add(exception.Message); + } + if (staged == null && failures.Count == 0) failures.Add("the start produced no run"); + return staged; + } + // Runs the scenario's Stage, then admits its player and everything it placed: each entity's stance set both ways // with the player, a pilot for each piloted one. Returns null, with every failure listed and the arena unchanged, // unless the whole scenario staged. diff --git a/Assets/Scripts/Zone Display/ZoneRenderer.cs b/Assets/Scripts/Zone Display/ZoneRenderer.cs index f63f746bd..24ce8a494 100644 --- a/Assets/Scripts/Zone Display/ZoneRenderer.cs +++ b/Assets/Scripts/Zone Display/ZoneRenderer.cs @@ -508,6 +508,7 @@ public class ZoneRenderer : MonoBehaviour void LateUpdate() { + if (!ActionGameManager.RunLive) return; Map.Draw(Zone, ActionGameManager.Clock.Lead); } @@ -550,6 +551,7 @@ public class ZoneRenderer : MonoBehaviour void Update() { + if (!ActionGameManager.RunLive) return; var lead = ActionGameManager.Clock.Lead; GravityLowering.Lower(Zone, lead, ref _gravityWells); foreach (var loot in _floatingItems.Values) diff --git a/tests/Aetheria.Shared.Tests/RunLaunchTests.cs b/tests/Aetheria.Shared.Tests/RunLaunchTests.cs new file mode 100644 index 000000000..1c8b0e380 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/RunLaunchTests.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Xunit; + +// RunStart.Launch decides whether a start produced a run; the game scene halts on its answer (a failed start clears the +// galaxy, and a scene with no galaxy steps nothing). Kills: a start that throws or returns nothing reading as a run, a +// failure with no reason to show on the menu, a thrown exception that is neither logged nor listed, and a start that +// worked being reported failed. +public sealed class RunLaunchTests +{ + [Fact] + public void AStartThatThrowsIsNoRunAndSaysWhy() + { + var failures = new List(); + var logged = new List(); + var staged = RunStart.Launch(() => throw new InvalidOperationException("the arena would not generate"), failures, logged.Add); + Assert.Null(staged); + Assert.Equal(new[] { "the arena would not generate" }, failures); + Assert.Single(logged); + } + + [Fact] + public void AStartThatReturnsNothingIsNoRunAndKeepsItsOwnReasons() + { + var failures = new List(); + Assert.Null(RunStart.Launch(() => { failures.Add("player: the scenario places no player"); return null; }, failures)); + Assert.Equal(new[] { "player: the scenario places no player" }, failures); + } + + [Fact] + public void AStartThatReturnsNothingAndSaysNothingStillGivesAReason() + { + var failures = new List(); + Assert.Null(RunStart.Launch(() => null, failures)); + Assert.Single(failures); + } + + [Fact] + public void AStartThatStagedIsARunWithNoFailures() + { + var failures = new List(); + var made = new RunStart.Staged(); + Assert.Same(made, RunStart.Launch(() => made, failures)); + Assert.Empty(failures); + } +}