Aetheria/Assets/Scripts/ServerShared/SavedGame.cs

276 lines
11 KiB
C#
Raw Permalink Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
using GameCult.Caching;
using MessagePack;
using CultMath;
using static CultMath.math;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[CultDocument("aetheria.savedgame", "1"), CultGlobal, MessagePackObject]
public class SavedGame
{
[Key(0)]
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
public CultRecordRef<SavedZone>[] Zones;
[Key(1)]
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
public CultRecordRef<Faction>[] Factions;
[Key(2)]
public Dictionary<int, int> HomeZones;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(3)]
public Dictionary<int, int> BossZones;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(4)]
public int Entrance;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(5)]
public int Exit;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(6)]
public int CurrentZone;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(7)]
public int CurrentZoneEntity;
[Key(8)]
public SectorBackgroundSettings Background;
[Key(9)]
public int[] DiscoveredZones;
[Key(10)]
public SavedActionBarBinding[] ActionBarBindings;
[Key(11)]
public bool IsTutorial;
[Key(12)]
public FactionRelationship[] Relationships;
[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 resource provinces (Galaxy.Provinces): each province's seed zone index and its type. Written only by
// RunSave.Capture and read only by Galaxy's save constructor; null in a save written before ore geography, which then
// gets Regions, Partition and Deal from its saved graph, boss zones, Exit and seed.
[Key(15)]
public List<SavedProvince> Provinces;
}
[MessagePackObject]
public class SavedProvince
{
[Key(0)]
public int Seed;
[Key(1)]
public CultRecordRef<ProvinceData> Type;
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
}
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
// The run's lifecycle over the run store. Only Commit creates SavedGame, SavedZone and ProvenanceLedger records;
// the ledger is the fifth run record.
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
public static class RunSave
{
// The live run as plain documents. Writes nothing.
public static (SavedGame Game, SavedZone[] Zones) Capture(CultCache cache, Galaxy galaxy, Zone currentZone,
Entity currentEntity, SavedActionBarBinding[] actionBar, int credits)
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
{
var factions = galaxy.HomeZones.Keys.ToArray();
var game = new SavedGame
{
DiscoveredZones = galaxy.DiscoveredZones.Select(dz => Array.IndexOf(galaxy.Zones, dz)).ToArray(),
Background = galaxy.Background,
Factions = factions.Select(f => cache.RefOf(f)).ToArray(),
Relationships = galaxy.Factions.Select(f => galaxy.FactionRelationships[f]).ToArray(),
HomeZones = galaxy.HomeZones.ToDictionary(
x => Array.IndexOf(factions, x.Key),
x => Array.IndexOf(galaxy.Zones, x.Value)),
BossZones = galaxy.BossZones.ToDictionary(
x => Array.IndexOf(factions, x.Key),
x => Array.IndexOf(galaxy.Zones, x.Value)),
CurrentZone = Array.FindIndex(galaxy.Zones, zone => zone.Contents == currentZone),
CurrentZoneEntity = currentZone.Entities.IndexOf(currentEntity),
Entrance = Array.IndexOf(galaxy.Zones, galaxy.Entrance),
Exit = Array.IndexOf(galaxy.Zones, galaxy.Exit),
IsTutorial = galaxy.IsPrelude,
ActionBarBindings = actionBar,
Credits = credits,
Seed = galaxy.Seed
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
};
// A zone generated earlier but not loaded this session has no live Contents; its packed contents carry over.
var zones = galaxy.Zones.Select(zone => new SavedZone
{
Name = zone.Name,
Position = zone.Position,
AdjacentZones = zone.AdjacentZones.Select(az => Array.IndexOf(galaxy.Zones, az)).ToArray(),
Factions = zone.Factions.Select(f => Array.IndexOf(factions, f)).ToArray(),
Contents = zone.Contents?.PackZone() ?? zone.PackedContents,
Owner = zone.Owner == null ? -1 : Array.IndexOf(factions, zone.Owner),
Province = Array.IndexOf(galaxy.Zones, zone.Province.Seed)
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
}).ToArray();
game.Provinces = galaxy.Provinces.Select(province => new SavedProvince
{Seed = Array.IndexOf(galaxy.Zones, province.Seed), Type = province.Type}).ToList();
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
return (game, zones);
}
// Continue's gate: every design a saved run names (each zone's items and each minted lot) must be one this catalog
// holds. A run made with a mod that is no longer installed is refused, naming the mod ships, before any scene is
// built; the save is never edited to fit.
public static void RequireDesigns(CultCache cache, SavedGame game)
{
var designs = new List<CultRecordKey>();
foreach (var zoneRef in game.Zones ?? Array.Empty<CultRecordRef<SavedZone>>())
{
var where = zoneRef.Key.Value;
var zone = cache.Get(zoneRef) ?? throw MalformedSave($"zone {where} has no record");
// A zone never visited has no contents: legitimately null.
foreach (var pack in zone.Contents?.Entities ?? new List<EntityPack>())
foreach (var item in EntitySerializer.Items(pack))
designs.Add((item ?? throw MalformedSave($"an item in {where} is null")).Data.Key);
}
foreach (var (number, lot) in Lots(cache).Lots ?? throw MalformedSave("the lot ledger has no lots"))
designs.Add((lot ?? throw MalformedSave($"lot {number} is null")).Design.Key);
var named = designs.Where(key => key.IsSet()).Distinct()
.Where(key => cache.Get<ItemData>(key) == null)
.Select(key => key.Value).OrderBy(key => key, StringComparer.Ordinal).ToArray();
if (named.Length == 0) return;
const string modHull = "mod-hull:";
var mods = named.Where(key => key.StartsWith(modHull, StringComparison.Ordinal)).Select(key => key.Substring(modHull.Length)).ToArray();
var others = named.Where(key => !key.StartsWith(modHull, StringComparison.Ordinal)).ToArray();
throw new InvalidOperationException("This run names designs the catalog no longer holds" +
(mods.Length > 0 ? $"; missing mod ships: {string.Join(", ", mods)}" : "") +
(others.Length > 0 ? $"; missing other designs: {string.Join(", ", others)}" : "") +
". Reinstall them, or start a new game.");
}
private static InvalidOperationException MalformedSave(string where) =>
new InvalidOperationException($"This run's save is malformed: {where}. Start a new game.");
// The stored ledger, or a fresh empty one when the run has minted nothing yet.
public static ProvenanceLedger Lots(CultCache cache) => cache.GetGlobal<ProvenanceLedger>() ?? new ProvenanceLedger();
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
// The only writer of SavedGame and SavedZone: one Commit to the run store. Zone i lands at savedzone-{i}, stable
// because a run's zone array is fixed at generation, and every stored SavedZone outside that set is removed. The
// run store's single-file commit writes its whole view, so orbits and bodies staged by zone generation land too.
// The ledger lands too, pruned to what the committed zones' entities still reach; the live ledger is untouched.
public static void Commit(CultCache cache, SavedGame game, IReadOnlyList<SavedZone> zones, ProvenanceLedger lots)
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
{
var keys = zones.Select((_, i) => new CultRecordKey($"savedzone-{i}")).ToArray();
var kept = new HashSet<CultRecordKey>(keys);
var stale = cache.AllStoredDocuments
.Where(stored => stored.Document is SavedZone && !kept.Contains(stored.Key))
.Select(stored => stored.Key)
.ToArray();
game.Zones = keys.Select(key => new CultRecordRef<SavedZone>(key)).ToArray();
var roots = zones
.SelectMany(zone => zone.Contents?.Entities ?? new List<EntityPack>())
.SelectMany(EntitySerializer.Items)
.OfType<CraftedItemInstance>()
.Select(item => item.Lot);
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
cache.Commit(batch =>
{
for (var i = 0; i < keys.Length; i++) batch.Upsert(typeof(SavedZone), zones[i], keys[i]);
batch.Upsert(game);
batch.Upsert(lots.Reachable(roots));
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
foreach (var key in stale) batch.Remove(key);
});
}
// Removes every run record (SavedGame, SavedZone, OrbitData, BodyData, ProvenanceLedger) in one Commit.
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
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
{
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
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
.Where(stored => IsRunRecord(stored.Descriptor.DocumentType))
.Select(stored => stored.Key)
.ToArray();
private static void Remove(CultCache cache, CultRecordKey[] records) =>
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
cache.Commit(batch =>
{
foreach (var key in records) batch.Remove(key);
Land RunSave and the one-cache run and player lifecycle (Cut 10) Owner: ActionGameManager composes the process's one cache. Its CultCache getter attaches the catalog, GameData/run.cc and GameData/player.cc on first access, in either scene, and nothing reopens it. - RunSave (SavedGame.cs) replaces the SavedGame constructor: - Capture builds the live run as plain documents and writes nothing. Zone contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a zone generated earlier but not loaded this session keeps its contents and its orbit and body records instead of being erased by the next save. - Commit is the only writer of SavedGame and SavedZone: one Commit with zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every stored SavedZone outside that set removed. Orbits and bodies staged by zone generation land in the same single-file write. - Clear removes every run record in one Commit. - Run begins: New Game's first statement is RunSave.Clear. Run saves: ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die clears the run, then saves settings. - Run exists only as the run store's SavedGame global: MainMenu enables Continue from it (and logs when the run store holds records but no SavedGame), and StartGame reads it once to pick the new-run or resume branch. - PlayerSettings: the getter returns the player store's global and, on first launch, commits GetDefaultPlayerSettings; it is the only creator. SavePlayerSettings commits the instance. - InputDisplayLayout reads its InputLayout from the catalog by the layout asset's name and throws when the catalog has none. Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant, SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore, ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each fails with its rule mutated out. Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
});
public static bool IsRunRecord(Type documentType) =>
AetheriaStores.RunTypes.Any(home => home.IsAssignableFrom(documentType));
}
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[CultDocument("aetheria.savedzone", "1"), MessagePackObject]
public class SavedZone
{
[Key(0)]
public string Name;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(1)]
public float2 Position;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(2)]
public int[] AdjacentZones;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(3)]
public int[] Factions;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(4)]
public int Owner;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(5)]
public ZonePack Contents;
// The seed zone index of the zone's province. Written only by RunSave.Capture and read only by Galaxy's save
// constructor; meaningful only when SavedGame.Provinces is present.
[Key(6)]
public int Province;
}
[MessagePackObject,
Union(0, typeof(SavedActionBarConsumableBinding)),
Union(1, typeof(SavedActionBarGearBinding)),
Union(2, typeof(SavedActionBarWeaponGroupBinding))
]
public abstract class SavedActionBarBinding
{
}
[MessagePackObject]
public class SavedActionBarConsumableBinding : SavedActionBarBinding
{
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
[Key(0)] public CultRecordRef<ConsumableItemData> Target;
}
[MessagePackObject]
public class SavedActionBarGearBinding : SavedActionBarBinding
{
[Key(0)] public int EquipmentIndex;
[Key(1)] public int BehaviorIndex;
}
[MessagePackObject]
public class SavedActionBarWeaponGroupBinding : SavedActionBarBinding
{
[Key(0)] public int Group;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
}