A new run replaces the saved run only once it has staged; a failed start says why and leaves the saved run

Launch cleared the run store before generating, and a generation or staging failure only reached the log: the
Generating Galaxy dialog stayed up, StartGame threw, and the saved run was already gone.

RunSave.Replace runs a start with the saved run still stored: when it returns a staged run the saved run's records go,
and when it returns null or throws, only the records it wrote (the arena's orbits and bodies) go. StartGame's
new-run branch is StartScenario, chosen by PendingScenario rather than by the absence of a SavedGame; it mints into an
empty ledger, and a failed start clears CurrentGalaxy (so quitting saves nothing) and refuses on the main menu with
every failure. Launch no longer clears; a galaxy that throws is refused on the title menu. The menu offers Continue
whenever no run is live, which replaces MainMenu.InGame (cut, with its prefab value and scene override), so a
failed start can resume the saved run in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-03 22:03:50 +02:00
commit 8a1e205cf0
6 changed files with 132 additions and 38 deletions

View file

@ -17,7 +17,6 @@ public class MainMenu : MonoBehaviour
public VolumeCloudRenderer CloudRenderer;
public GameSettings Settings;
public ConfirmationDialog Dialog;
public bool InGame;
public Prototype PanelPrototype;
public float FadeTime = .5f;
public float FadeDistance = 512;
@ -92,9 +91,10 @@ public class MainMenu : MonoBehaviour
{
_nextMenu.panel.Clear();
_nextMenu.panel.Title.text = TitleSubtitle("aetheria", "terminus");
if (!InGame)
if (ActionGameManager.CurrentGalaxy == null)
{
// A run exists only as the run store's SavedGame global.
// No run is live: at the title, after death, or after a new run failed to start. A saved run exists only as
// the run store's SavedGame global.
var cache = ActionGameManager.CultCache;
if (ActionGameManager.ModExclusions.Length > 0 && !_exclusionsShown)
{
@ -156,11 +156,11 @@ public class MainMenu : MonoBehaviour
}
}
// The only way into a new run: clears the saved run, generates the scenario's galaxy off the main thread, and
// enters the game, where StartGame has RunStart stage it.
// The only way into a new run: generates the scenario's galaxy off the main thread and enters the game, where
// StartGame has RunStart stage it and only then replaces the saved run. A galaxy that cannot generate is refused
// here, with the saved run untouched.
private void Launch(Scenario scenario)
{
RunSave.Clear(ActionGameManager.CultCache);
var generatorState = "Loading Database Contents";
Fade(true);
@ -181,16 +181,21 @@ public class MainMenu : MonoBehaviour
ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"),
Debug.Log,
state => generatorState = state);
Task.Run(() =>
{
var galaxy = RunStart.Generate(scenario, stage);
Task.Run(() => RunStart.Generate(scenario, stage)).ContinueWith(generation =>
Observable.NextFrame().Subscribe(_ =>
{
ActionGameManager.CurrentGalaxy = galaxy;
if (generation.IsFaulted)
{
Debug.LogException(generation.Exception);
ShowMain();
Fade(false);
Refuse($"{scenario.Name} could not generate its galaxy", generation.Exception.GetBaseException().Message);
return;
}
ActionGameManager.CurrentGalaxy = generation.Result;
ActionGameManager.PendingScenario = scenario;
EnterGame();
});
}).ContinueWith(t => Debug.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}));
}
// The one way into the game scene. Mod ship prototypes import asynchronously at boot, so the scene waits for them,
@ -209,7 +214,7 @@ public class MainMenu : MonoBehaviour
SceneManager.LoadScene("ARPG");
}
private void Refuse(string title, string reason)
public void Refuse(string title, string reason)
{
Debug.LogError($"{title}: {reason}");
Dialog.Clear();