From 65a6297aee247c603141850eaf0be2dc7048f989 Mon Sep 17 00:00:00 2001 From: Nibu Date: Sat, 7 Nov 2026 11:55:50 +0000 Subject: [PATCH] Save standings by faction, not by list position RunSave.Capture wrote Factions in HomeZones key order and Relationships in galaxy.Factions order; Galaxy's save constructor paired them by index, so a galaxy whose two orders differed resumed with standings on the wrong factions. SavedGame.Standings (Key 16) now carries each standing with its faction ref. A save without it still reads Relationships by index, the only record it holds. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/ServerShared/Galaxy.cs | 12 +++-- Assets/Scripts/ServerShared/SavedGame.cs | 20 +++++++- tests/Aetheria.Shared.Tests/RunSaveTests.cs | 53 +++++++++++++++++++-- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/Assets/Scripts/ServerShared/Galaxy.cs b/Assets/Scripts/ServerShared/Galaxy.cs index 08512f8af..797e77764 100644 --- a/Assets/Scripts/ServerShared/Galaxy.cs +++ b/Assets/Scripts/ServerShared/Galaxy.cs @@ -58,10 +58,14 @@ public class Galaxy Seed = savedGame.Seed; Factions = savedGame.Factions.Select(faction => cultCache.Get(faction)).ToArray(); - for (var i = 0; i < Factions.Length; i++) - { - FactionRelationships[Factions[i]] = savedGame.Relationships[i]; - } + if (savedGame.Standings != null) + foreach (var standing in savedGame.Standings) + FactionRelationships[cultCache.Get(standing.Faction)] = standing.Relationship; + else + // A save before Standings wrote Relationships in an order it did not record; it is right only for a + // galaxy that never reordered, and is all that save holds. + for (var i = 0; i < Factions.Length; i++) + FactionRelationships[Factions[i]] = savedGame.Relationships[i]; var savedZones = savedGame.Zones.Select(zone => cultCache.Get(zone)).ToArray(); Zones = savedZones.Select(zone => diff --git a/Assets/Scripts/ServerShared/SavedGame.cs b/Assets/Scripts/ServerShared/SavedGame.cs index 90ef03566..ff4de6e9b 100644 --- a/Assets/Scripts/ServerShared/SavedGame.cs +++ b/Assets/Scripts/ServerShared/SavedGame.cs @@ -45,6 +45,8 @@ public class SavedGame [Key(11)] public bool IsTutorial; + // Read only by a save written before Standings: its entries follow galaxy.Factions order, not Factions order, so + // Galaxy trusts it only when the two orders cannot differ (see Galaxy's save constructor). [Key(12)] public FactionRelationship[] Relationships; @@ -61,6 +63,21 @@ public class SavedGame // gets Regions, Partition and Deal from its saved graph, boss zones, Exit and seed. [Key(15)] public List Provinces; + + // Each faction's standing, keyed by the faction itself so no list order can misplace it. Written by RunSave.Capture; + // null in a save written before it, which then reads Relationships. + [Key(16)] + public List Standings; +} + +[MessagePackObject] +public class SavedStanding +{ + [Key(0)] + public CultRecordRef Faction; + + [Key(1)] + public FactionRelationship Relationship; } [MessagePackObject] @@ -87,7 +104,8 @@ public static class RunSave 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(), + Standings = galaxy.Factions.Select(f => new SavedStanding + {Faction = cache.RefOf(f), Relationship = galaxy.FactionRelationships[f]}).ToList(), HomeZones = galaxy.HomeZones.ToDictionary( x => Array.IndexOf(factions, x.Key), x => Array.IndexOf(galaxy.Zones, x.Value)), diff --git a/tests/Aetheria.Shared.Tests/RunSaveTests.cs b/tests/Aetheria.Shared.Tests/RunSaveTests.cs index 00a6baa75..05c0a86ac 100644 --- a/tests/Aetheria.Shared.Tests/RunSaveTests.cs +++ b/tests/Aetheria.Shared.Tests/RunSaveTests.cs @@ -150,6 +150,48 @@ public sealed class RunSaveTests : IDisposable } } + // Galaxy.Factions and Galaxy.HomeZones.Keys are two orders of the same factions. A saved standing belongs to its + // faction, whichever order the galaxy lists them in. + [Fact] + public void StandingsSurviveSaveAndContinueWhenFactionOrderDiffersFromHomeZoneOrder() + { + using (var cache = AetheriaStores.Open(Catalog, catalogWritable: true)) + { + cache.Upsert(new Faction { Name = "Borealis", ShortName = "BOR" }); + cache.Upsert(new Faction { Name = "Corvid", ShortName = "COR" }); + cache.FlushAsync().Wait(); + } + + using (var cache = Open()) + { + var factions = cache.GetAll().OrderBy(f => f.Name, StringComparer.Ordinal).ToArray(); + var game = Game(cache, factions); + RunSave.Commit(cache, game, Zones(2, null, -1), new ProvenanceLedger()); + } + + var expected = new Dictionary + { + { "Adrasteia", FactionRelationship.Hated }, { "Borealis", FactionRelationship.Friendly }, { "Corvid", FactionRelationship.Beloved } + }; + + using (var cache = Open()) + { + var galaxy = new Galaxy(cache, cache.GetGlobal(), _ => { }); + Assert.NotEqual(galaxy.Factions, galaxy.HomeZones.Keys); + foreach (var faction in galaxy.Factions) galaxy.FactionRelationships[faction] = expected[faction.Name]; + var itemManager = new ItemManager(1u, 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); + RunSave.Commit(cache, game, zones, RunSave.Lots(cache)); + } + + using (var cache = Open()) + { + var galaxy = new Galaxy(cache, cache.GetGlobal(), _ => { }); + Assert.Equal(expected, galaxy.Factions.ToDictionary(f => f.Name, f => galaxy.FactionRelationships[f])); + } + } + // The run's credits ride the save: what Capture is given is what a later Continue reads from the run store. [Fact] public void CreditsSurviveContinue() @@ -753,11 +795,14 @@ public sealed class RunSaveTests : IDisposable return new ZonePack { Orbits = { root, moon }, Planets = { body } }; } - private static SavedGame Game(CultCache cache) => new SavedGame + private static SavedGame Game(CultCache cache) => Game(cache, cache.GetAll().Single()); + + // HomeZones lists the factions in reverse of their saved order. + private static SavedGame Game(CultCache cache, params Faction[] factions) => new SavedGame { - Factions = new[] { cache.RefOf(cache.GetAll().Single()) }, - Relationships = new[] { FactionRelationship.Neutral }, - HomeZones = new Dictionary { { 0, 0 } }, + Factions = factions.Select(f => cache.RefOf(f)).ToArray(), + Relationships = factions.Select(_ => FactionRelationship.Neutral).ToArray(), + HomeZones = factions.Select((_, i) => factions.Length - 1 - i).ToDictionary(i => i, _ => 0), BossZones = new Dictionary(), DiscoveredZones = new[] { 0 }, ActionBarBindings = new SavedActionBarBinding[0], -- 2.47.3