From 020dc58a812c04d6e8fb983d32d1bf0a1d3095ee Mon Sep 17 00:00:00 2001
From: Meta Vens
Date: Sun, 18 Oct 2026 17:29:17 +0200
Subject: [PATCH] The run's seed rides the save: SavedGame key 14, written by
Capture, read by Continue
Galaxy.Seed on a continued run is the saved seed instead of zero. A save
written before run-seed decodes Seed 0.
Co-Authored-By: Claude Sonnet 5.5
---
Assets/Scripts/ServerShared/Galaxy.cs | 5 ++-
Assets/Scripts/ServerShared/GalaxyStage.cs | 3 +-
Assets/Scripts/ServerShared/SavedGame.cs | 8 +++-
.../DemoTerminusTests.cs | 2 +
tests/Aetheria.Shared.Tests/RunSaveTests.cs | 37 +++++++++++++++++++
5 files changed, 51 insertions(+), 4 deletions(-)
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()
{