Loadouts.Commit takes bossOf: the Loadout and Faction.BossLoadout land in one batch
With bossOf set, the same conditional batch Expects the faction unchanged and Upserts it with BossLoadout = KeyOf(Name), so both land or neither does. The batch lives in an internal overload over a caller's cache; tests pin that a faction changed on disk after the read refuses both, and that a missing boss faction writes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7f1337167d
commit
80283d18a9
2 changed files with 92 additions and 9 deletions
|
|
@ -96,19 +96,28 @@ public static class Loadouts
|
|||
_ => ""
|
||||
};
|
||||
|
||||
// The only writer of Loadout records. It opens its own short-lived cache over the catalog file, writable, and disposes it
|
||||
// after the one commit. No other cache is touched, so no catalog instance play holds (or has mutated) can reach the
|
||||
// file; a cache that already has the catalog open sees the loadout only once it reloads.
|
||||
// The catalog is a stack of packages, the shipped content included (ruling catalog-is-a-package-stack), so a loadout may
|
||||
// name any design the stack holds; one whose package is later removed fails closed where it is built, with its reason logged.
|
||||
// Throws and writes nothing when the file is missing (capture never creates a catalog), when a loadout of this name
|
||||
// exists under another key, or when one exists under KeyOf(Name) and replace is not set. The commit is conditional
|
||||
// on the record at KeyOf(Name), so a loadout changed on disk since this open is not clobbered: returns false then.
|
||||
public static bool Commit(string catalogPath, Loadout loadout, bool replace)
|
||||
// The only writer of Loadout records, and of the Faction.BossLoadout that names one. It opens its own short-lived cache
|
||||
// over the catalog file, writable, and disposes it after the one commit. No other cache is touched, so no catalog
|
||||
// instance play holds (or has mutated) can reach the file; a cache that already has the catalog open sees the loadout
|
||||
// only once it reloads. The catalog is a stack of packages, the shipped content included (ruling
|
||||
// catalog-is-a-package-stack), so a loadout may name any design the stack holds; one whose package is later removed fails
|
||||
// closed where it is built, with its reason logged. With bossOf set, the same batch makes the loadout that faction's boss
|
||||
// (Faction.BossLoadout), so the loadout and the reference land together or not at all.
|
||||
// Throws and writes nothing when the file is missing (capture never creates a catalog), when bossOf names no faction in
|
||||
// it, when a loadout of this name exists under another key, or when one exists under KeyOf(Name) and replace is not set.
|
||||
// The commit is conditional on the record at KeyOf(Name) and on the faction, so a record changed on disk since this open
|
||||
// is not clobbered: returns false then, and neither is written.
|
||||
public static bool Commit(string catalogPath, Loadout loadout, bool replace, CultRecordRef<Faction> bossOf = default)
|
||||
{
|
||||
if (!File.Exists(catalogPath))
|
||||
throw new InvalidOperationException($"Catalog {catalogPath} does not exist; a loadout commit never creates one.");
|
||||
using var cache = AetheriaStores.Open(catalogPath, catalogWritable: true);
|
||||
return Commit(cache, loadout, replace, bossOf);
|
||||
}
|
||||
|
||||
// The one batch Commit(path, ...) makes, over a writable catalog cache the caller opened.
|
||||
internal static bool Commit(CultCache cache, Loadout loadout, bool replace, CultRecordRef<Faction> bossOf)
|
||||
{
|
||||
var key = KeyOf(loadout.Name);
|
||||
var namesake = cache.GetAll<Loadout>().FirstOrDefault(other => other.Name == loadout.Name && !cache.RefOf(other).Key.Equals(key));
|
||||
if (namesake != null)
|
||||
|
|
@ -116,10 +125,17 @@ public static class Loadouts
|
|||
var existing = cache.Get<Loadout>(key);
|
||||
if (existing != null && !replace)
|
||||
throw new InvalidOperationException($"Loadout '{loadout.Name}' already exists; replace it explicitly.");
|
||||
var boss = bossOf.IsSet()
|
||||
? cache.Get(bossOf) ?? throw new InvalidOperationException($"Loadout '{loadout.Name}': the boss faction is not in the catalog.")
|
||||
: null;
|
||||
return cache.Commit(batch =>
|
||||
{
|
||||
batch.Expect(key, existing);
|
||||
batch.Upsert(typeof(Loadout), loadout, key);
|
||||
if (boss == null) return;
|
||||
batch.Expect(bossOf.Key, boss);
|
||||
boss.BossLoadout = new CultRecordRef<Loadout>(key);
|
||||
batch.Upsert(boss.GetType(), boss, bossOf.Key);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,73 @@ public sealed class LoadoutTests : IDisposable
|
|||
Assert.Equal(key, (where == "hull" ? written.Hull.Key : written.Slots[1].Design.Key).Value);
|
||||
}
|
||||
|
||||
// With bossOf, one commit writes the loadout and makes it that faction's boss: Faction.BossLoadout names its key.
|
||||
[Fact]
|
||||
public void ABossCommitWritesTheLoadoutAndTheFactionsBossReference()
|
||||
{
|
||||
using (var cache = Open())
|
||||
Assert.True(Loadouts.Commit(Catalog, HandBuilt(cache), replace: false, cache.RefOf(cache.GetByName<Faction>("Maker"))));
|
||||
|
||||
using (var cache = Open())
|
||||
{
|
||||
var loadout = Assert.Single(cache.GetAll<Loadout>());
|
||||
Assert.Equal(Loadouts.KeyOf("Skiff build"), cache.RefOf(loadout).Key);
|
||||
Assert.Same(loadout, Loadouts.BossOf(cache, cache.GetByName<Faction>("Maker"), out var refusal));
|
||||
Assert.Equal(BossRefusal.None, refusal);
|
||||
}
|
||||
}
|
||||
|
||||
// The loadout and the boss reference are one batch: when the faction changed on disk after the commit's cache read it,
|
||||
// the commit returns false and neither lands, though the loadout alone would have; the other writer's change survives.
|
||||
[Fact]
|
||||
public void ABossCommitOverAFactionChangedOnDiskWritesNeitherTheLoadoutNorTheReference()
|
||||
{
|
||||
Loadout loadout;
|
||||
CultRecordRef<Faction> maker;
|
||||
using (var read = Open())
|
||||
{
|
||||
loadout = HandBuilt(read);
|
||||
maker = read.RefOf(read.GetByName<Faction>("Maker"));
|
||||
}
|
||||
|
||||
using (var stale = AetheriaStores.Open(Catalog, catalogWritable: true))
|
||||
{
|
||||
Assert.NotNull(stale.Get(maker));
|
||||
using (var other = AetheriaStores.Open(Catalog, catalogWritable: true))
|
||||
{
|
||||
var fresh = other.Get(maker);
|
||||
Assert.True(other.Commit(batch =>
|
||||
{
|
||||
batch.Expect(maker.Key, fresh);
|
||||
fresh.ShortName = "NEW";
|
||||
batch.Upsert(typeof(Faction), fresh, maker.Key);
|
||||
}));
|
||||
}
|
||||
|
||||
Assert.False(Loadouts.Commit(stale, loadout, replace: false, maker));
|
||||
}
|
||||
|
||||
using var after = Open();
|
||||
Assert.Empty(after.GetAll<Loadout>());
|
||||
var faction = after.Get(maker);
|
||||
Assert.Equal("NEW", faction.ShortName);
|
||||
Assert.False(faction.BossLoadout.IsSet());
|
||||
}
|
||||
|
||||
// A boss faction the catalog does not hold is refused before anything is written.
|
||||
[Fact]
|
||||
public void ABossCommitNamingNoFactionIsRefusedAndNothingIsWritten()
|
||||
{
|
||||
var before = Hash(Catalog);
|
||||
using (var cache = Open())
|
||||
{
|
||||
var error = Assert.Throws<InvalidOperationException>(() =>
|
||||
Loadouts.Commit(Catalog, HandBuilt(cache), replace: false, new CultRecordRef<Faction>(new CultRecordKey("faction:absent"))));
|
||||
Assert.Contains("boss faction is not in the catalog", error.Message);
|
||||
}
|
||||
Assert.Equal(before, Hash(Catalog));
|
||||
}
|
||||
|
||||
// Recapturing a name refuses without replace and leaves the file untouched; with replace it replaces, never duplicates.
|
||||
[Fact]
|
||||
public void SameNameCaptureReplacesOnlyWhenAsked()
|
||||
|
|
|
|||
Loading…
Reference in a new issue