diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 5de02a6b1..ff63ed55e 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Aetheria.State.Smoke/Program.cs b/Aetheria.State.Smoke/Program.cs index 840bb3476..281a65ae3 100644 --- a/Aetheria.State.Smoke/Program.cs +++ b/Aetheria.State.Smoke/Program.cs @@ -286,6 +286,7 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s ExitZoneIndex = 1, CurrentZoneIndex = 0, CurrentZoneEntityIndex = 0, + GenerationSeed = 424242, DiscoveredZoneIndices = [0], ZoneKeys = [zoneKey.ToString()], ActionBarBindings = @@ -441,6 +442,7 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether } if (runState?.RunId != "smoke" || + runState.GenerationSeed != 424242 || runState.ZoneKeys.Length != 1 || runState.ActionBarBindings.Length != 1) { diff --git a/Aetheria.State.Unity.Smoke/Program.cs b/Aetheria.State.Unity.Smoke/Program.cs index 60875a3df..bec8bb386 100644 --- a/Aetheria.State.Unity.Smoke/Program.cs +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -216,6 +216,7 @@ try new AetheriaRuntimeRunCheckpointCommit { RunId = "smoke-run", + GenerationSeed = 424242, CurrentZoneIndex = 0, CurrentZoneEntityIndex = 0, DiscoveredZoneIndices = new[] { 0 }, @@ -504,6 +505,7 @@ try loadout.RootEntity.CargoContents.Length == 1 && loadout.RootEntity.CargoContents[0].Items[0].Item.Quantity == 5 && run?.ZoneKeys.Length == 1 && + run.GenerationSeed == 424242 && run.ActionBarBindings.Length == 2 && run.ActionBarBindings[0].ControlPath == "/1" && run.ActionBarBindings[0].Kind == "weapon_group" && @@ -593,6 +595,7 @@ try packageLoadouts[0].RootEntity.CargoContents[0].Items[0].Item.Quantity != 5 || packageRuns.Count != 1 || packageRuns[0].RunId != "smoke-run" || + packageRuns[0].GenerationSeed != 424242 || packageRuns[0].ZoneKeys.Count != 1 || packageRuns[0].ActionBarBindings.Count != 2 || packageRuns[0].ActionBarBindings[1].TargetKey != "aetheria.item_definition:legacy:smoke:weapon" || diff --git a/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs b/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs index 994ad3b6c..d6648999d 100644 --- a/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs +++ b/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs @@ -227,7 +227,8 @@ public static class AetheriaRuntimeCommitLogApplier DiscoveredZoneIndices = ToIntArray(checkpoint.DiscoveredZoneIndices), ActionBarBindings = ToActionBarBindings(checkpoint.ActionBarBindings), FactionRelationships = ToFactionRelationships(checkpoint.FactionRelationships), - UpdatedAtUtc = updatedAtUtc + UpdatedAtUtc = updatedAtUtc, + GenerationSeed = checkpoint.GenerationSeed }; var zoneKeys = new List(); diff --git a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs index abd5d2bfe..bfe0ca8d3 100644 --- a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs +++ b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs @@ -225,6 +225,9 @@ public sealed class AetheriaRunState [Key(10)] public string UpdatedAtUtc { get; set; } = ""; + + [Key(11)] + public uint GenerationSeed { get; set; } } [CultDocument("aetheria.zone_state", "aetheria.zone_state.v1")] diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 2b9f83ea3..0b114d78e 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -538,6 +538,7 @@ public class ActionGameManager : MonoBehaviour ExitZoneIndex = ZoneIndex(CurrentGalaxy?.Exit), CurrentZoneIndex = ZoneIndex(Zone?.GalaxyZone), CurrentZoneEntityIndex = Zone?.Entities?.IndexOf(CurrentEntity) ?? -1, + GenerationSeed = CurrentGalaxy?.GenerationSeed ?? 0, DiscoveredZoneIndices = CurrentGalaxy?.DiscoveredZones .Select(ZoneIndex) .Where(index => index >= 0) diff --git a/Assets/Scripts/ServerShared/Galaxy.cs b/Assets/Scripts/ServerShared/Galaxy.cs index 92de808ec..26953c503 100644 --- a/Assets/Scripts/ServerShared/Galaxy.cs +++ b/Assets/Scripts/ServerShared/Galaxy.cs @@ -26,6 +26,7 @@ public class Galaxy public GalaxyZone[] Zones { get; } public GalaxyZone Entrance { get; } public GalaxyZone Exit { get; } + public uint GenerationSeed { get; } public Dictionary FactionRelationships { get; } = new Dictionary(); private Action Log { get; } public bool IsPrelude { get; } @@ -60,7 +61,8 @@ public class Galaxy IsPrelude = false; Background = background; Log = log; - var random = new Random(seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed); + GenerationSeed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed; + var random = new Random(GenerationSeed); Factions = _allFactions.OrderBy(x => random.NextFloat()).Take(settings.MegaCount).ToArray(); foreach (var f in Factions) FactionRelationships[f] = FactionRelationship.Neutral; @@ -117,7 +119,8 @@ public class Galaxy Background = background; Log = log; - var random = new Random(seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed); + GenerationSeed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed; + var random = new Random(GenerationSeed); var factions = new List(); diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index 93ba5f5aa..9e5fd64f3 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -107,6 +107,7 @@ public class MainMenu : MonoBehaviour { Settings.SectorBackgroundSettings.NoisePosition = Random.value * 1000; ActionGameManager.IsTutorial = false; + var generationSeed = NextGenerationSeed(); Task.Run(() => { var sector = new Galaxy( @@ -115,7 +116,8 @@ public class MainMenu : MonoBehaviour Settings.NameGeneratorSettings, ActionGameManager.RuntimeCatalog, Debug.Log, - setState); + setState, + generationSeed); Observable.NextFrame().Subscribe(_ => { ActionGameManager.CurrentGalaxy = sector; @@ -133,6 +135,7 @@ public class MainMenu : MonoBehaviour } while (Settings.TutorialBackgroundSettings.CloudDensity(float2(0.5f)) < .5f); ActionGameManager.IsTutorial = true; + var generationSeed = NextGenerationSeed(); Task.Run(() => { var sector = new Galaxy( @@ -143,7 +146,8 @@ public class MainMenu : MonoBehaviour ActionGameManager.RuntimePlayerSettings, ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"), Debug.Log, - setState); + setState, + generationSeed); Observable.NextFrame().Subscribe(_ => { ActionGameManager.CurrentGalaxy = sector; @@ -161,6 +165,12 @@ public class MainMenu : MonoBehaviour _nextMenu.panel.AddButton("Quit", Application.Quit); } + private static uint NextGenerationSeed() + { + var seed = (uint)Random.Range(1, int.MaxValue); + return seed == 0 ? 1u : seed; + } + private void ShowSettings() { _nextMenu.panel.Clear(); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs index 4aead2bad..d12d89bfc 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs @@ -687,7 +687,8 @@ namespace GameCult.Aetheria.State.Unity IReadOnlyList zoneKeys, IReadOnlyList actionBarBindings, IReadOnlyList factionRelationships, - string updatedAtUtc) + string updatedAtUtc, + uint generationSeed) { RunId = runId; IsTutorial = isTutorial; @@ -700,6 +701,7 @@ namespace GameCult.Aetheria.State.Unity ActionBarBindings = actionBarBindings; FactionRelationships = factionRelationships; UpdatedAtUtc = updatedAtUtc; + GenerationSeed = generationSeed; } public string RunId { get; } @@ -713,6 +715,7 @@ namespace GameCult.Aetheria.State.Unity public IReadOnlyList ActionBarBindings { get; } public IReadOnlyList FactionRelationships { get; } public string UpdatedAtUtc { get; } + public uint GenerationSeed { get; } } public sealed class AetheriaRuntimeZoneStateSnapshot diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs index d4257595c..87ac8401e 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -462,7 +462,8 @@ namespace GameCult.Aetheria.State.Unity var actionBarBindings = ReadFieldActionBarBindings(ref reader, fields, 8); var factionRelationships = ReadFieldFactionRelationships(ref reader, fields, 9); var updatedAtUtc = ReadFieldString(ref reader, fields, 10); - SkipRemaining(ref reader, fields, 11); + var generationSeed = ReadFieldUInt32(ref reader, fields, 11); + SkipRemaining(ref reader, fields, 12); return new AetheriaRuntimeRunStateSnapshot( runId, isTutorial, @@ -474,7 +475,8 @@ namespace GameCult.Aetheria.State.Unity zoneKeys, actionBarBindings, factionRelationships, - updatedAtUtc); + updatedAtUtc, + generationSeed); } private static AetheriaRuntimeZoneStateSnapshot ReadZoneStatePayload(byte[] payload) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs index 4163a2557..3b3fa66e8 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs @@ -238,6 +238,9 @@ namespace GameCult.Aetheria.State.Unity [Key(9)] public IReadOnlyList FactionRelationships { get; set; } = Array.Empty(); + + [Key(10)] + public uint GenerationSeed { get; set; } } [MessagePackObject] diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index dcb113cbf..6ffe1aca2 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -40,9 +40,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. `AetheriaStateNode`. The embedded Unity state package can also read typed run, zone, and entity snapshot documents back from the `.cc` store as read-only runtime DTOs, including action-bar bindings, faction - relationships, zone entity/orbit/body references, entity session scalars, - active consumables, behavior progress, weapon state, behavior state, and - stat-grid rows. Local run saves, loadouts, player settings files, zone + relationships, generation seed, zone entity/orbit/body references, entity + session scalars, active consumables, behavior progress, weapon state, + behavior state, and stat-grid rows. Local run saves, loadouts, player settings files, zone files, and generated keyboard layout caches no longer write bespoke durable files. The legacy `RuntimePlayerSettings` runtime object is no longer named or decorated as a @@ -122,8 +122,8 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. - `Aetheria.State` now exposes typed node put/get ports for run state, zone state, and entity snapshots. The state smoke writes a run referencing a zone, a zone referencing an entity snapshot, and an entity snapshot carrying - position, direction, faction, hull, equipment slots, weapon groups, and a stat - grid. This proves the `.zone` replacement graph is durable typed state. Unity + generation seed, position, direction, faction, hull, equipment slots, weapon + groups, and a stat grid. This proves the `.zone` replacement graph is durable typed state. Unity now queues current-zone/current-entity-collection snapshots, current action-bar bindings, and current faction relationship rows through the runtime commit log during run checkpoints; `RuntimeZoneBlueprint` and @@ -136,6 +136,12 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. Runtime blueprints no longer capture or restore behavior-private `PersistentBehaviorData` blobs; that dead save-shape hook and its `IPersistentBehavior` interface are deleted. +- Galaxy generation now exposes the actual nonzero seed used to create topology. + The main menu passes explicit seeds into normal and tutorial galaxy + construction, and run checkpoints persist that seed into `AetheriaRunState`. + Current-zone snapshot restore must attach to a regenerated galaxy with the + same seed; restoring zone indices against a fresh random galaxy is forbidden + split-brain state, not a Continue feature. - Run checkpoint zone snapshots include typed orbit and body rows. Orbit IDs, parent IDs, distance, phase, fixed positions, body kind/name/orbit, resources, gravity/body multipliers, asteroid belt entries, and gas/sun @@ -791,6 +797,9 @@ First Aetheria surfaces to publish: entity snapshot documents, so the `.zone` replacement graph is not only writable through the state node but also inspectable by runtime package consumers. + - Done: persist the actual galaxy generation seed in typed run state, so + future Continue/restore work has a reproducible topology body before + applying current-zone/entity snapshots. - Remaining: add typed documents/mappers for runtime object graphs, typed behavior factory construction, remaining behavior-private state not covered by progress, weapon, sensor, radiator, reactor, or capacitor rows,