diff --git a/Assets/Scripts/ServerShared/Galaxy.cs b/Assets/Scripts/ServerShared/Galaxy.cs index b2d1e5dd0..e9e75b239 100644 --- a/Assets/Scripts/ServerShared/Galaxy.cs +++ b/Assets/Scripts/ServerShared/Galaxy.cs @@ -28,8 +28,8 @@ public class Galaxy private Action Log { get; } public bool IsPrelude { get; } - // The seed this galaxy was generated at, which also seeds its run's item draws (RunStart); zero for a galaxy - // restored from a save. + // The run's seed, minted once by GalaxyStage.SeedFrom and carried by the save (SavedGame.Seed). It seeds + // generation, the run's item draws (RunStart) and the zone's drop dice (Zone.DropSeed). public uint Seed { get; } private GalaxyZone[] _exitPath; @@ -51,6 +51,7 @@ public class Galaxy _cache = cultCache; Log = log; Background = savedGame.Background; + Seed = savedGame.Seed; Factions = savedGame.Factions.Select(faction => cultCache.Get(faction)).ToArray(); for (var i = 0; i < Factions.Length; i++) diff --git a/Assets/Scripts/ServerShared/GalaxyStage.cs b/Assets/Scripts/ServerShared/GalaxyStage.cs index cd8feefed..a93e25f89 100644 --- a/Assets/Scripts/ServerShared/GalaxyStage.cs +++ b/Assets/Scripts/ServerShared/GalaxyStage.cs @@ -46,7 +46,8 @@ public sealed class GalaxyStage _clock = clock ?? (() => (uint) (DateTime.Now.Ticks % uint.MaxValue)); } - // The seed this generation runs at, never zero: the scenario's, or the clock's when the scenario's is zero. + // The run's one minter: the scenario's seed, or the clock's when the scenario's is zero, never zero. Galaxy carries + // what it mints and SavedGame keeps it, so Continue rolls the same drops. public uint Seed { get; private set; } internal void SeedFrom(Scenario scenario) diff --git a/Assets/Scripts/ServerShared/SavedGame.cs b/Assets/Scripts/ServerShared/SavedGame.cs index ba819e6ec..1758074fa 100644 --- a/Assets/Scripts/ServerShared/SavedGame.cs +++ b/Assets/Scripts/ServerShared/SavedGame.cs @@ -50,6 +50,11 @@ public class SavedGame [Key(13)] public int Credits; + + // The run's seed (Galaxy.Seed), minted once by GalaxyStage.SeedFrom. Written only by RunSave.Capture and read only + // by Galaxy's save constructor; 0 in a save written before run-seed, which then rolls name-only drops. + [Key(14)] + public uint Seed; } // The run's lifecycle over the run store. Only Commit creates SavedGame, SavedZone and ProvenanceLedger records; @@ -79,7 +84,8 @@ public static class RunSave Exit = Array.IndexOf(galaxy.Zones, galaxy.Exit), IsTutorial = galaxy.IsPrelude, ActionBarBindings = actionBar, - Credits = credits + Credits = credits, + Seed = galaxy.Seed }; // A zone generated earlier but not loaded this session has no live Contents; its packed contents carry over. diff --git a/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs b/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs index ae1895e75..54f46a57c 100644 --- a/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs +++ b/tests/Aetheria.Shared.Tests/DemoTerminusTests.cs @@ -51,6 +51,8 @@ public sealed partial class RunStartTests var (game, zones) = RunSave.Capture(_cache, galaxy, arena, staged.Player, new SavedActionBarBinding[0], 0); RunSave.Commit(_cache, game, zones, _items.Lots); var restored = new Galaxy(_cache, _cache.GetGlobal(), _ => { }); + Assert.NotEqual(0u, galaxy.Seed); + Assert.Equal(galaxy.Seed, restored.Seed); Dictionary Standing(Galaxy g) => g.Factions.ToDictionary(faction => faction.Name, faction => g.FactionRelationships[faction]); Assert.Equal(new Dictionary diff --git a/tests/Aetheria.Shared.Tests/RunSaveTests.cs b/tests/Aetheria.Shared.Tests/RunSaveTests.cs index 3bd075724..1320b7ddd 100644 --- a/tests/Aetheria.Shared.Tests/RunSaveTests.cs +++ b/tests/Aetheria.Shared.Tests/RunSaveTests.cs @@ -170,6 +170,43 @@ public sealed class RunSaveTests : IDisposable Assert.Equal(4242, cache.GetGlobal().Credits); } + // The run's seed rides the save: Continue restores it, Capture writes it back, and a save from before run-seed + // restores zero. + [Fact] + public void TheRunSeedRidesTheSave() + { + using (var cache = Open()) + { + var game = Game(cache); + game.Seed = 0xC0FFEEu; + RunSave.Commit(cache, game, Zones(2, StageZoneContents(cache), 0), new ProvenanceLedger()); + } + + using (var cache = Open()) + { + var galaxy = new Galaxy(cache, cache.GetGlobal(), _ => { }); + Assert.Equal(0xC0FFEEu, galaxy.Seed); + var itemManager = new ItemManager(cache, new ProvenanceLedger(), TestSettings(), _ => { }); + galaxy.Zones[0].Contents = new Zone(itemManager, new PlanetSettings(), new ZonePack(), galaxy.Zones[0], galaxy); + var (game, zones) = RunSave.Capture(cache, galaxy, galaxy.Zones[0].Contents, null, new SavedActionBarBinding[0], 0); + Assert.Equal(0xC0FFEEu, game.Seed); + RunSave.Commit(cache, game, zones, RunSave.Lots(cache)); + } + + using (var cache = Open()) + Assert.Equal(0xC0FFEEu, cache.GetGlobal().Seed); + + // A save written before run-seed holds no seed, and continues on name-only drops. + using (var cache = Open()) + RunSave.Commit(cache, Game(cache), Zones(2, StageZoneContents(cache), 0), new ProvenanceLedger()); + + using (var cache = Open()) + { + Assert.Equal(0u, cache.GetGlobal().Seed); + Assert.Equal(0u, new Galaxy(cache, cache.GetGlobal(), _ => { }).Seed); + } + } + [Fact] public void ClearRemovesEveryRunRecord() {