Continue refuses a run whose equipment no longer fits its hull's edited layout #6
2 changed files with 71 additions and 1 deletions
|
|
@ -158,7 +158,7 @@ public static class RunSave
|
|||
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;
|
||||
if (named.Length == 0) { RequireLayouts(cache, game); 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();
|
||||
|
|
@ -168,6 +168,35 @@ public static class RunSave
|
|||
". Reinstall them, or start a new game.");
|
||||
}
|
||||
|
||||
// Continue's second gate: a hull's cells or hardpoints edited under a saved run must still hold what the run
|
||||
// equipped on it (a package edited in place keeps its key, so the design check above passes). Each saved piece of
|
||||
// gear must sit where equipping would accept it on the hull as the catalog holds it now, by the same rules
|
||||
// (HardpointData.TakesAt for hardpoint gear, interior cells for tools); occupancy is not rechecked, the save
|
||||
// carries only what equipping admitted.
|
||||
private static void RequireLayouts(CultCache cache, SavedGame game)
|
||||
{
|
||||
var moved = new SortedSet<string>(StringComparer.Ordinal);
|
||||
void Check(EntityPack pack)
|
||||
{
|
||||
if (pack.Hull.Data.Key.IsSet() && cache.Get<ItemData>(pack.Hull.Data.Key) is HullData hull)
|
||||
foreach (var (position, item) in pack.Equipment.Concat(pack.CargoBays).Concat(pack.DockingBays))
|
||||
{
|
||||
if (!item.Data.Key.IsSet() || !(cache.Get<ItemData>(item.Data.Key) is EquippableItemData design)) continue;
|
||||
var fits = design.HardpointType == HardpointType.Tool
|
||||
? design.Shape.Coordinates.All(cell => hull.InteriorCells[position + design.Shape.Rotate(cell, item.Rotation)])
|
||||
: hull.Hardpoints.Any(hardpoint => hardpoint.TakesAt(design, position - hardpoint.Position));
|
||||
if (!fits) moved.Add(hull.Name);
|
||||
}
|
||||
foreach (var child in pack.Children) Check(child);
|
||||
}
|
||||
foreach (var zoneRef in game.Zones ?? Array.Empty<CultRecordRef<SavedZone>>())
|
||||
foreach (var pack in cache.Get(zoneRef).Contents?.Entities ?? new List<EntityPack>())
|
||||
Check(pack);
|
||||
if (moved.Count > 0)
|
||||
throw new InvalidOperationException("This run's equipment no longer fits its hulls' layouts; changed hulls: " +
|
||||
string.Join(", ", moved) + ". Restore the earlier version of the mod, or start a new game.");
|
||||
}
|
||||
|
||||
private static InvalidOperationException MalformedSave(string where) =>
|
||||
new InvalidOperationException($"This run's save is malformed: {where}. Start a new game.");
|
||||
|
||||
|
|
|
|||
|
|
@ -592,6 +592,47 @@ public sealed class RunSaveTests : IDisposable
|
|||
Assert.Equal(before, File.ReadAllBytes(Run));
|
||||
}
|
||||
|
||||
// Continue from a hull's edited layout: a mod package edited in place keeps its key, so the design check passes, but
|
||||
// gear saved against the old hardpoints must not be loaded onto the new ones (equipping would silently drop it).
|
||||
[Fact]
|
||||
public void RunWhoseHullHardpointMovedRefusesContinueNamingTheHull()
|
||||
{
|
||||
var hullKey = new CultRecordKey("mod-hull:mod.skiff");
|
||||
HullData Skiff(int2 hardpointAt) => new HullData
|
||||
{
|
||||
Name = "Skiff",
|
||||
Shape = new Shape(4, 4),
|
||||
Hardpoints = new List<HardpointData> { new HardpointData { Type = HardpointType.Sensors, Position = hardpointAt, Shape = new Shape() } }
|
||||
};
|
||||
CultRecordRef<ItemData> lamp;
|
||||
using (var cache = AetheriaStores.Open(Catalog, catalogWritable: true))
|
||||
{
|
||||
cache.UpsertAsync(typeof(HullData), Skiff(new int2(1, 1)), hullKey).GetAwaiter().GetResult();
|
||||
lamp = new CultRecordRef<ItemData>(cache.Upsert(new GearData { Name = "Lamp", Hardpoint = HardpointType.Sensors, Shape = new Shape() }).Key);
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
using (var cache = Open())
|
||||
{
|
||||
var pack = BarePack(new EquippableItem { Data = new CultRecordRef<ItemData>(hullKey), Lot = 1 });
|
||||
pack.Equipment = new[] { (new int2(1, 1), new EquippableItem { Data = lamp, Lot = 1 }) };
|
||||
var lots = new ProvenanceLedger { NextLot = 2 };
|
||||
lots.Lots[1] = new Lot { Origin = new Attributed() };
|
||||
RunSave.Commit(cache, Game(cache), new[] { ZoneOf(pack) }, lots);
|
||||
}
|
||||
using (var cache = Open()) RunSave.RequireDesigns(cache, cache.GetGlobal<SavedGame>());
|
||||
|
||||
using (var cache = AetheriaStores.Open(Catalog, catalogWritable: true))
|
||||
{
|
||||
cache.UpsertAsync(typeof(HullData), Skiff(new int2(3, 3)), hullKey).GetAwaiter().GetResult();
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
using (var cache = Open())
|
||||
{
|
||||
var refusal = Assert.Throws<InvalidOperationException>(() => RunSave.RequireDesigns(cache, cache.GetGlobal<SavedGame>()));
|
||||
Assert.Contains("changed hulls: Skiff", refusal.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// --- RequireDesigns: what it walks, what it lists ---------------------------------------------------------------
|
||||
|
||||
// Commits a run whose zones the caller builds, then returns RequireDesigns' refusal message, or null when it passes.
|
||||
|
|
|
|||
Loading…
Reference in a new issue