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:
parent
02e7fd4b7e
commit
8a1e205cf0
6 changed files with 132 additions and 38 deletions
|
|
@ -915,7 +915,6 @@ MonoBehaviour:
|
|||
m_EditorClassIdentifier:
|
||||
Settings: {fileID: 11400000, guid: cd2344e2001a37544aab0960c164d4ff, type: 2}
|
||||
Dialog: {fileID: 4533226043449861657}
|
||||
InGame: 0
|
||||
PanelPrototype: {fileID: 7329274282942065594}
|
||||
FadeTime: 0.5
|
||||
FadeDistance: 1024
|
||||
|
|
|
|||
|
|
@ -9636,11 +9636,6 @@ PrefabInstance:
|
|||
propertyPath: m_SizeDelta.y
|
||||
value: 90.91
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4533226043457584178, guid: 3d8d9cbf10b12214e98891c1f03c1e99,
|
||||
type: 3}
|
||||
propertyPath: InGame
|
||||
value: 1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 4533226043457584178, guid: 3d8d9cbf10b12214e98891c1f03c1e99,
|
||||
type: 3}
|
||||
propertyPath: CloudRenderer
|
||||
|
|
|
|||
|
|
@ -270,7 +270,9 @@ public class ActionGameManager : MonoBehaviour
|
|||
EntityInstance.EffectManagerParent = EffectManagerParent;
|
||||
ConsoleController.MessageReceiver = this;
|
||||
|
||||
ItemManager = new ItemManager(CultCache, RunSave.Lots(CultCache), Settings.GameplaySettings, Debug.Log);
|
||||
// A new run mints into an empty ledger; the saved run's stays in the store until the new run has started.
|
||||
ItemManager = new ItemManager(CultCache, PendingScenario != null ? new ProvenanceLedger() : RunSave.Lots(CultCache),
|
||||
Settings.GameplaySettings, Debug.Log);
|
||||
ZoneRenderer.ItemManager = ItemManager;
|
||||
|
||||
// If hiding minimap asteroids, turn them off to start with
|
||||
|
|
@ -795,22 +797,10 @@ public class ActionGameManager : MonoBehaviour
|
|||
{
|
||||
if (CurrentGalaxy != null)
|
||||
{
|
||||
var saved = CultCache.GetGlobal<SavedGame>();
|
||||
if (saved == null)
|
||||
{
|
||||
var scenario = PendingScenario ?? throw new InvalidOperationException("A new run needs a scenario; MainMenu.Launch sets one.");
|
||||
PendingScenario = null;
|
||||
SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance));
|
||||
// RunStart owns what a new run starts with (docs/scenarios-cut.md, R.4); this only enters and binds.
|
||||
RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, CurrentGalaxy, scenario);
|
||||
PopulateLevel(CurrentGalaxy.Entrance);
|
||||
var failures = new List<string>();
|
||||
var staged = RunStart.Stage(ItemManager, Zone, scenario, Settings.StartingHullName, Settings.TutorialGenerationSettings, failures)
|
||||
?? throw new InvalidOperationException($"Scenario {scenario.Name} could not stage: {string.Join("; ", failures)}");
|
||||
BindToEntity(staged.Player);
|
||||
}
|
||||
if (PendingScenario != null) StartScenario();
|
||||
else
|
||||
{
|
||||
var saved = CultCache.GetGlobal<SavedGame>();
|
||||
foreach(var group in CurrentGalaxy.DiscoveredZones
|
||||
.GroupBy(dz=>dz.Distance[CurrentGalaxy.Entrance]))
|
||||
SectorMap.QueueZoneReveal(group);
|
||||
|
|
@ -835,6 +825,41 @@ public class ActionGameManager : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
// RunStart owns what a new run starts with (docs/scenarios-cut.md, R.4); this enters and binds. The saved run is
|
||||
// replaced only once the scenario has staged. A start that fails leaves the saved run as it was and says why on
|
||||
// the main menu, where Continue still resumes the saved run.
|
||||
private void StartScenario()
|
||||
{
|
||||
var scenario = PendingScenario;
|
||||
PendingScenario = null;
|
||||
var failures = new List<string>();
|
||||
RunStart.Staged staged = null;
|
||||
try
|
||||
{
|
||||
staged = RunSave.Replace(CultCache, () =>
|
||||
{
|
||||
RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, 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);
|
||||
}
|
||||
|
||||
if (staged == null)
|
||||
{
|
||||
CurrentGalaxy = null;
|
||||
MainMenu.gameObject.SetActive(true);
|
||||
MainMenu.Refuse($"{scenario.Name} could not start", string.Join("\n", failures));
|
||||
return;
|
||||
}
|
||||
SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance));
|
||||
BindToEntity(staged.Player);
|
||||
}
|
||||
|
||||
private IEnumerator IntroCutscene(Ship ship)
|
||||
{
|
||||
ZoneRenderer.PerspectiveEntity = ship;
|
||||
|
|
|
|||
|
|
@ -155,17 +155,37 @@ public static class RunSave
|
|||
}
|
||||
|
||||
// Removes every run record (SavedGame, SavedZone, OrbitData, BodyData, ProvenanceLedger) in one Commit.
|
||||
public static void Clear(CultCache cache)
|
||||
public static void Clear(CultCache cache) => Remove(cache, Records(cache));
|
||||
|
||||
// A new run replaces the saved one only once it has started: start runs with the saved run still in the store.
|
||||
// When it returns a value, the saved run's records go. When it returns null or throws, the records it wrote go and
|
||||
// the saved run stays as it was.
|
||||
public static T Replace<T>(CultCache cache, Func<T> start) where T : class
|
||||
{
|
||||
var run = cache.AllStoredDocuments
|
||||
var saved = Records(cache);
|
||||
T started = null;
|
||||
try
|
||||
{
|
||||
started = start();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Remove(cache, started != null ? saved : Records(cache).Except(saved).ToArray());
|
||||
}
|
||||
return started;
|
||||
}
|
||||
|
||||
private static CultRecordKey[] Records(CultCache cache) =>
|
||||
cache.AllStoredDocuments
|
||||
.Where(stored => IsRunRecord(stored.Descriptor.DocumentType))
|
||||
.Select(stored => stored.Key)
|
||||
.ToArray();
|
||||
|
||||
private static void Remove(CultCache cache, CultRecordKey[] records) =>
|
||||
cache.Commit(batch =>
|
||||
{
|
||||
foreach (var key in run) batch.Remove(key);
|
||||
foreach (var key in records) batch.Remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
public static bool IsRunRecord(Type documentType) =>
|
||||
AetheriaStores.RunTypes.Any(home => home.IsAssignableFrom(documentType));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -126,6 +126,56 @@ public sealed class RunSaveTests : IDisposable
|
|||
Assert.Equal(before, new[] { Catalog, Player }.Select(Hash).ToArray());
|
||||
}
|
||||
|
||||
// A new run's start writes its arena's orbits and bodies while the saved run is still stored. A start that fails,
|
||||
// by returning nothing or by throwing, takes away only what it wrote; one that starts takes away the saved run.
|
||||
[Fact]
|
||||
public void ANewRunReplacesTheSavedRunOnlyOnceItHasStarted()
|
||||
{
|
||||
using (var cache = Open())
|
||||
RunSave.Commit(cache, Game(cache), Zones(3, StageZoneContents(cache), 0), new ProvenanceLedger());
|
||||
string[] saved;
|
||||
using (var cache = Open())
|
||||
saved = RunKeys(cache);
|
||||
Assert.Equal(8, saved.Length);
|
||||
|
||||
using (var cache = Open())
|
||||
Assert.Null(RunSave.Replace<ZonePack>(cache, () =>
|
||||
{
|
||||
StageZoneContents(cache);
|
||||
return null;
|
||||
}));
|
||||
using (var cache = Open())
|
||||
{
|
||||
Assert.Equal(saved, RunKeys(cache));
|
||||
Assert.NotNull(cache.GetGlobal<SavedGame>());
|
||||
}
|
||||
|
||||
using (var cache = Open())
|
||||
Assert.Throws<InvalidOperationException>(() => RunSave.Replace<ZonePack>(cache, () =>
|
||||
{
|
||||
StageZoneContents(cache);
|
||||
throw new InvalidOperationException("the arena would not generate");
|
||||
}));
|
||||
using (var cache = Open())
|
||||
Assert.Equal(saved, RunKeys(cache));
|
||||
|
||||
ZonePack started;
|
||||
using (var cache = Open())
|
||||
started = RunSave.Replace(cache, () => StageZoneContents(cache));
|
||||
using (var cache = Open())
|
||||
{
|
||||
Assert.Null(cache.GetGlobal<SavedGame>());
|
||||
Assert.Equal(started.Orbits.Select(orbit => orbit.Key.Value).Concat(started.Planets.Select(body => body.Key.Value))
|
||||
.OrderBy(key => key, StringComparer.Ordinal), RunKeys(cache));
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] RunKeys(CultCache cache) => cache.AllStoredDocuments
|
||||
.Where(stored => RunSave.IsRunRecord(stored.Descriptor.DocumentType))
|
||||
.Select(stored => stored.Key.Value)
|
||||
.OrderBy(key => key, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
// Lot 1 sits on the root ship's hull; lot 3 (Produced from facility 4, input 5) sits in a docked child's
|
||||
// docking-bay contents; lots 2 and 6 are minted but referenced by nothing packed. Only the reachable closure
|
||||
// {1, 3, 4, 5} survives into the written copy; the live ledger keeps every lot.
|
||||
|
|
|
|||
Loading…
Reference in a new issue