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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-18 17:29:17 +02:00
commit 020dc58a81
5 changed files with 51 additions and 4 deletions

View file

@ -28,8 +28,8 @@ public class Galaxy
private Action<string> 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++)

View file

@ -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)

View file

@ -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.

View file

@ -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<SavedGame>(), _ => { });
Assert.NotEqual(0u, galaxy.Seed);
Assert.Equal(galaxy.Seed, restored.Seed);
Dictionary<string, FactionRelationship> Standing(Galaxy g) => g.Factions.ToDictionary(faction => faction.Name, faction => g.FactionRelationships[faction]);
Assert.Equal(new Dictionary<string, FactionRelationship>

View file

@ -170,6 +170,43 @@ public sealed class RunSaveTests : IDisposable
Assert.Equal(4242, cache.GetGlobal<SavedGame>().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<SavedGame>(), _ => { });
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<SavedGame>().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<SavedGame>().Seed);
Assert.Equal(0u, new Galaxy(cache, cache.GetGlobal<SavedGame>(), _ => { }).Seed);
}
}
[Fact]
public void ClearRemovesEveryRunRecord()
{