Fix F1: PersistedBehaviors as a pairs array, per Cut 0's ruling

Dictionary<int2, PersistentBehaviorData[]> has no hash-resistant comparer
under CultCache's UntrustedData security, so any run store holding an
entity threw MessagePackSerializationException on reopen, even when the
dictionary was empty. EntityPack.PersistedBehaviors (key 5, unchanged)
becomes (int2 position, PersistentBehaviorData[] data)[], matching
Equipment/CargoBays/DockingBays. Pack writes pairs; Unpack builds a local
lookup before restoring behaviors.

PersistentBehaviorData had no concrete leaf and no Union, so nothing could
ever exercise its wire format; added MarkerPersistentBehaviorData as the
one placeholder leaf so ReopenedPackKeepsPersistedBehaviors can round-trip
real data ahead of a real IPersistentBehavior implementer landing.

Tests: RunSaveTests.BarePack uses an empty pairs array.
Added ReopenedPackKeepsPersistedBehaviors (a hand-built pack with two
positions, each with one persisted behavior, round-trips through commit
and reopen) and ReopenSurvivesAGeneratedShipWithEntities (a real ship from
LoadoutGenerator against a catalog fixture, committed into a SavedZone and
reopened, pinning the defect at the layer Soul's probe found it).
MaterializedLotsSurviveSaveAndReload and CommitKeepsOnlyReachableLots go
green; 36 of 36 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-17 04:58:15 +02:00
commit e4df73e4fe
3 changed files with 140 additions and 12 deletions

View file

@ -30,14 +30,16 @@ public static class EntitySerializer
pack.Settings = entity.Settings;
// Filter item behavior collections by those with any persistent behaviors
// For each item create an object containing the item position and a list of persistent behaviors
// Then turn that into a dictionary mapping from item position to an array of every behaviors persistent data
// For each item create a (position, data) pair holding every one of its behaviors' persistent data.
// A dictionary keyed by int2 has no hash-resistant comparer under CultCache's untrusted-data security, so
// this is an array of pairs, matching Equipment/CargoBays/DockingBays.
pack.PersistedBehaviors = entity.Equipment
.Where(item => item.Behaviors.Any(b=>b is IPersistentBehavior))
.Select(item => new {equippable=item, behaviors = item.Behaviors
.Select(item => (item.Position, item.Behaviors
.Where(b=>b is IPersistentBehavior)
.Cast<IPersistentBehavior>()})
.ToDictionary(x=> x.equippable.Position, x=>x.behaviors.Select(b => b.Store()).ToArray());
.Cast<IPersistentBehavior>()
.Select(b => b.Store()).ToArray()))
.ToArray();
pack.Hull = entity.Hull;
pack.Name = entity.Name;
@ -121,13 +123,15 @@ public static class EntitySerializer
entity.DockingBays[bayIndex].TryStore(item, position);
// Iterate only over the behaviors of items which contain persistent data
// Filter the behaviors for each item to get the persistent ones, then cast them and combine with the persisted data array for that item
// Build a local lookup from the pairs array, then filter each item's behaviors for the persistent ones,
// cast them and combine with that item's persisted data array.
var persistedBehaviorsByPosition = pack.PersistedBehaviors.ToDictionary(x => x.position, x => x.data);
foreach (var persistentBehaviorData in entity.Equipment
.Where(item => pack.PersistedBehaviors.ContainsKey(item.Position))
.Where(item => persistedBehaviorsByPosition.ContainsKey(item.Position))
.SelectMany(item => item.Behaviors
.Where(b=> b is IPersistentBehavior)
.Cast<IPersistentBehavior>()
.Zip(pack.PersistedBehaviors[item.Position], (behavior, data) => new{behavior, data})))
.Zip(persistedBehaviorsByPosition[item.Position], (behavior, data) => new{behavior, data})))
persistentBehaviorData.behavior.Restore(persistentBehaviorData.data);
entity.Temperature = pack.Temperature;
@ -197,7 +201,7 @@ public abstract class EntityPack
[Key(2)] public (int2 position, EquippableItem item)[] Equipment;
[Key(3)] public (int2 position, EquippableItem item)[] CargoBays;
[Key(4)] public (int2 position, EquippableItem item)[] DockingBays;
[Key(5)] public Dictionary<int2, PersistentBehaviorData[]> PersistedBehaviors;
[Key(5)] public (int2 position, PersistentBehaviorData[] data)[] PersistedBehaviors;
[Key(6)] public float[,] Temperature;
[Key(7)] public float[,] Armor;
[Key(8)] public bool2[,] Conductivity;