Save standings by faction, not by list position #2

Merged
nibu merged 1 commit from nibu/saved-relationships-order into master 2026-11-10 17:45:16 +00:00
3 changed files with 76 additions and 9 deletions

View file

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

View file

@ -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<SavedProvince> 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<SavedStanding> Standings;
}
[MessagePackObject]
public class SavedStanding
{
[Key(0)]
public CultRecordRef<Faction> 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)),

View file

@ -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<Faction>().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<string, FactionRelationship>
{
{ "Adrasteia", FactionRelationship.Hated }, { "Borealis", FactionRelationship.Friendly }, { "Corvid", FactionRelationship.Beloved }
};
using (var cache = Open())
{
var galaxy = new Galaxy(cache, cache.GetGlobal<SavedGame>(), _ => { });
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<SavedGame>(), _ => { });
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<Faction>().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<Faction>().Single()) },
Relationships = new[] { FactionRelationship.Neutral },
HomeZones = new Dictionary<int, int> { { 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<int, int>(),
DiscoveredZones = new[] { 0 },
ActionBarBindings = new SavedActionBarBinding[0],