Merge minor-power-factions (eureka/aetheria-release-minor-power-factions, 7d1106a9)
Code only: Aya, Megiddo and Forge as ordinary factions, the Pirates allegiance top-up, ten minor-power products. Also carries the ItemData.ResolveStatField fix: the static stat-field cache is a ConcurrentDictionary (Soul judged ItemData the right owner), because parallel catalog opens raced on it. The branch's GameData/Aetheria.cc is not taken; the catalog is regenerated by replay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
c486b06d04
8 changed files with 365 additions and 15 deletions
|
|
@ -818,8 +818,8 @@ public static class StatValidation
|
|||
// ApplyModifier threw an unhelpful NullReferenceException at random equip time. Resolving it here, once,
|
||||
// memoized, and failing loudly at catalog load or Upsert (through ValidateStatModifiers below) names the
|
||||
// record instead of leaving the bug for whoever equips the item first.
|
||||
private static readonly Dictionary<(string Target, string Stat), (Type Type, FieldInfo Field)> _resolvedStatFields =
|
||||
new Dictionary<(string, string), (Type, FieldInfo)>();
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<(string Target, string Stat), (Type Type, FieldInfo Field)> _resolvedStatFields =
|
||||
new System.Collections.Concurrent.ConcurrentDictionary<(string, string), (Type, FieldInfo)>();
|
||||
|
||||
private static Type[] _statReferenceTypes;
|
||||
private static Type[] StatReferenceTypes => _statReferenceTypes ??= typeof(BehaviorData).GetAllChildClasses()
|
||||
|
|
|
|||
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
Binary file not shown.
146
tests/Aetheria.Shared.Tests/MinorPowersTests.cs
Normal file
146
tests/Aetheria.Shared.Tests/MinorPowersTests.cs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using Xunit;
|
||||
|
||||
// The Aya Collective, Megiddo and Orbital Forge (aetheria-release, cut minor-power-factions; ruling no-territory-ban): three
|
||||
// ordinary factions in the shipped catalog, authored by AetherDb's minor-power-factions command, which a scratch copy of the
|
||||
// catalog without them proves idempotent and conflict-refusing.
|
||||
public sealed class MinorPowersTests : IDisposable
|
||||
{
|
||||
private static readonly string[] Minor = { "Aya", "Megiddo", "Forge" };
|
||||
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "minor-powers-" + Guid.NewGuid().ToString("N"));
|
||||
private string CatalogPath => Path.Combine(_root, "GameData", "Aetheria.cc");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_root)) Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
// A scratch copy of the shipped catalog with the three, their products and every allegiance toward them removed, through a
|
||||
// registry scoped to the shipped assembly (as PiratesFactionCommandTests does), so the command has all of it to author.
|
||||
private void StripTheMinorPowers()
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(_root, "GameData"));
|
||||
File.Copy(Path.Combine(AetherDb.FindRoot(), "GameData", "Aetheria.cc"), CatalogPath);
|
||||
using var cache = new CultCache(TestCatalog.Registry());
|
||||
cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes);
|
||||
var gone = cache.GetAll<Faction>().Where(f => Minor.Contains(f.ShortName)).Select(f => cache.RefOf(f)).ToArray();
|
||||
Assert.Equal(3, gone.Length);
|
||||
foreach (var product in cache.GetAll<FactionProductData>().Where(p => gone.Any(g => g.Key.Equals(p.Manufacturer.Key))).ToArray())
|
||||
Assert.True(cache.Remove(cache.RefOf(product).Key));
|
||||
foreach (var reference in gone) Assert.True(cache.Remove(reference.Key));
|
||||
foreach (var faction in cache.GetAll<Faction>())
|
||||
foreach (var key in faction.Allegiance.Keys.Where(k => gone.Any(g => g.Key.Equals(k.Key))).ToArray())
|
||||
faction.Allegiance.Remove(key);
|
||||
cache.Commit(batch =>
|
||||
{
|
||||
foreach (var faction in cache.GetAll<Faction>()) batch.Upsert(typeof(Faction), faction, cache.RefOf(faction).Key);
|
||||
});
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheMinorPowersAreOrdinaryFactions()
|
||||
{
|
||||
using var cache = RestoredHullsTests.OpenCatalog();
|
||||
var factions = cache.GetAll<Faction>().ToArray();
|
||||
foreach (var shortName in Minor)
|
||||
{
|
||||
var faction = Assert.Single(factions, f => f.ShortName == shortName);
|
||||
Assert.NotNull(cache.Get(faction.GeonameFile));
|
||||
Assert.True(faction.InfluenceDistance > 0, shortName);
|
||||
var named = faction.Allegiance.Keys.Select(k => k.Key).ToArray();
|
||||
var expected = factions.Where(f => f != faction && f.ShortName != "Pirates").Select(f => cache.RefOf(f).Key).ToArray();
|
||||
Assert.Equal(expected.Length, named.Length);
|
||||
Assert.All(expected, key => Assert.Contains(named, n => n.Equals(key)));
|
||||
}
|
||||
// Territory is nothing a faction can say of itself: no member of Faction marks a maker-only or landless power.
|
||||
Assert.DoesNotContain(typeof(Faction).GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic),
|
||||
m => m.Name.Contains("ManufacturerOnly") || m.Name.Contains("Territory"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheCommandIsIdempotentAndRefusesAConflict()
|
||||
{
|
||||
StripTheMinorPowers();
|
||||
var before = File.ReadAllBytes(CatalogPath);
|
||||
Assert.Equal(0, MinorPowerFactionsCommand.Run(new string[0], _root));
|
||||
Assert.Equal(before, File.ReadAllBytes(CatalogPath));
|
||||
|
||||
Assert.Equal(0, MinorPowerFactionsCommand.Run(new[] { "apply" }, _root));
|
||||
var landed = File.ReadAllBytes(CatalogPath);
|
||||
Assert.NotEqual(before, landed);
|
||||
var db = AetherDb.Open(root: _root);
|
||||
try
|
||||
{
|
||||
var factions = db.Cache.GetAll<Faction>().ToArray();
|
||||
foreach (var shortName in Minor) Assert.Single(factions, f => f.ShortName == shortName);
|
||||
foreach (var shortName in Minor)
|
||||
{
|
||||
var own = factions.Single(f => f.ShortName == shortName);
|
||||
var expected = factions.Where(f => f != own && f.ShortName != "Pirates").Select(f => db.Cache.RefOf(f).Key).ToArray();
|
||||
Assert.Equal(expected.Length, own.Allegiance.Count);
|
||||
Assert.All(expected, key => Assert.Contains(own.Allegiance.Keys, k => k.Key.Equals(key)));
|
||||
}
|
||||
var forge = db.Cache.RefOf(factions.Single(f => f.ShortName == "Forge"));
|
||||
Assert.Equal(.5f, factions.Single(f => f.ShortName == "AU").Allegiance[forge]);
|
||||
// The Pirates' allegiance is pirates-faction's; Miss Terri's is her own.
|
||||
Assert.All(factions.Where(f => f.ShortName is "Pirates" or "Miss Terri's"), f => Assert.DoesNotContain(f.Allegiance.Keys, k => k.Equals(forge)));
|
||||
}
|
||||
finally { db.Cache.Dispose(); }
|
||||
|
||||
Assert.Equal(0, MinorPowerFactionsCommand.Run(new[] { "apply" }, _root));
|
||||
Assert.Equal(landed, File.ReadAllBytes(CatalogPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ANameHeldUnderAnotherKeyIsRefusedWithNothingWritten()
|
||||
{
|
||||
StripTheMinorPowers();
|
||||
var squatter = AetherDb.Open(catalogWritable: true, root: _root);
|
||||
squatter.Cache.Commit(batch => batch.Upsert(typeof(Faction), new Faction { Name = "Megiddo", ShortName = "Megiddo" }));
|
||||
squatter.Cache.Dispose();
|
||||
var before = File.ReadAllBytes(CatalogPath);
|
||||
Assert.Throws<InvalidOperationException>(() => MinorPowerFactionsCommand.Run(new[] { "apply" }, _root));
|
||||
Assert.Equal(before, File.ReadAllBytes(CatalogPath));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class RunStartTests
|
||||
{
|
||||
// Forge holds no zone in the prelude, so its gear reaches AU only through allegiance (ruling gear-without-presence); a
|
||||
// design-only slot draws among the makers AU can reach (ruling design-only-weighted-draw). Miss Terri's names none of them.
|
||||
[Fact]
|
||||
public void TheirGearSellsWhereTheyAreAbsent()
|
||||
{
|
||||
var factions = _cache.GetAll<Faction>().ToArray();
|
||||
var forge = factions.Single(f => f.ShortName == "Forge");
|
||||
Assert.DoesNotContain(_galaxy.Factions, f => f == forge);
|
||||
var autocannon = _cache.GetAll<CraftedItemData>().Single(d => d.Name == "Autocannon");
|
||||
var pattern30 = _cache.GetAll<FactionProductData>().Single(p => p.Name == "Pattern 30");
|
||||
|
||||
LoadoutGenerator For(string shortName, uint seed)
|
||||
{
|
||||
_items.Random = new CultMath.Random(seed);
|
||||
return new LoadoutGenerator(ref _items.Random, _items, _galaxy, _galaxy.Entrance, factions.Single(f => f.ShortName == shortName), .5f);
|
||||
}
|
||||
Assert.True(For("AU", 1).IsAvailable(pattern30));
|
||||
// Seeds 1-64 spread by a multiplicative hash: the generator's first draws of adjacent small seeds are close together.
|
||||
Assert.Contains(Enumerable.Range(1, 64), seed => For("AU", unchecked((uint) seed * 2654435761u)).ChooseProduct(autocannon)?.Name == pattern30.Name);
|
||||
|
||||
var terri = For("Miss Terri's", 1);
|
||||
var theirs = _cache.GetAll<FactionProductData>().Where(p => new[] { "Aya", "Megiddo", "Forge" }.Contains(_cache.Get(p.Manufacturer).ShortName)).ToArray();
|
||||
Assert.Equal(10, theirs.Length);
|
||||
Assert.All(theirs, product => Assert.False(terri.IsAvailable(product), product.Name));
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,13 @@ public sealed class PiratesFactionCommandTests : IDisposable
|
|||
|
||||
public void Dispose() => Directory.Delete(_root, true);
|
||||
|
||||
private int AllFactionsButThePirates()
|
||||
{
|
||||
var db = AetherDb.Open(root: _root);
|
||||
try { return db.Cache.GetAll<Faction>().Count(f => f.Name != "Pirates"); }
|
||||
finally { db.Cache.Dispose(); }
|
||||
}
|
||||
|
||||
private (Faction[] Pirates, FactionProductData[] Products) Read()
|
||||
{
|
||||
var db = AetherDb.Open(root: _root);
|
||||
|
|
@ -69,7 +76,7 @@ public sealed class PiratesFactionCommandTests : IDisposable
|
|||
Assert.Equal(3, faction.InfluenceDistance);
|
||||
Assert.Equal(new CultMath.float3(.42f, .40f, .37f), faction.PrimaryColor);
|
||||
Assert.Equal(new CultMath.float3(1f, .45f, .05f), faction.SecondaryColor);
|
||||
Assert.Equal(12, faction.Allegiance.Count);
|
||||
Assert.Equal(AllFactionsButThePirates(), faction.Allegiance.Count);
|
||||
Assert.All(faction.Allegiance.Values, value => Assert.Equal(1f, value));
|
||||
|
||||
var db = AetherDb.Open(root: _root);
|
||||
|
|
@ -105,17 +112,52 @@ public sealed class PiratesFactionCommandTests : IDisposable
|
|||
Assert.Equal(landed, File.ReadAllBytes(CatalogPath));
|
||||
}
|
||||
|
||||
// An existing hand-authored Pirates record is topped up, never rewritten (follow-up pirates-allegiance-and-identity): it keeps
|
||||
// its description, short name and record key and gains no products; after a faction is added, apply gives allegiance 1.0 to
|
||||
// that faction alone; a second apply leaves the file bytes unchanged and a dry run writes nothing.
|
||||
[Fact]
|
||||
public void An_existing_Pirates_faction_is_left_as_it_is()
|
||||
public void An_existing_Pirates_faction_is_topped_up_with_allegiance_and_nothing_else()
|
||||
{
|
||||
var db = AetherDb.Open(catalogWritable: true, root: _root);
|
||||
db.Cache.Commit(batch => batch.Upsert(typeof(Faction), new Faction { Name = "Pirates", ShortName = "Mine", Description = "authored by hand" }));
|
||||
var key = db.Cache.RefOf(db.Cache.GetAll<Faction>().Single(f => f.Name == "Pirates")).Key;
|
||||
db.Cache.Dispose();
|
||||
|
||||
var beforeDry = File.ReadAllBytes(CatalogPath);
|
||||
Assert.Equal(0, Program.PiratesFaction(apply: false, root: _root));
|
||||
Assert.Equal(beforeDry, File.ReadAllBytes(CatalogPath));
|
||||
|
||||
Assert.Equal(0, Program.PiratesFaction(apply: true, root: _root));
|
||||
var (pirates, products) = Read();
|
||||
Assert.Equal("authored by hand", Assert.Single(pirates).Description);
|
||||
var record = Assert.Single(pirates);
|
||||
Assert.Equal("authored by hand", record.Description);
|
||||
Assert.Equal("Mine", record.ShortName);
|
||||
Assert.Empty(products);
|
||||
var topped = File.ReadAllBytes(CatalogPath);
|
||||
|
||||
var late = AetherDb.Open(catalogWritable: true, root: _root);
|
||||
late.Cache.Commit(batch => batch.Upsert(typeof(Faction), new Faction { Name = "Latecomer", ShortName = "Late" }));
|
||||
var lateRef = late.Cache.RefOf(late.Cache.GetAll<Faction>().Single(f => f.Name == "Latecomer"));
|
||||
var others = late.Cache.GetAll<Faction>().Count(f => f.Name != "Pirates");
|
||||
late.Cache.Dispose();
|
||||
|
||||
Assert.Equal(0, Program.PiratesFaction(apply: true, root: _root));
|
||||
var check = AetherDb.Open(root: _root);
|
||||
try
|
||||
{
|
||||
var after = check.Cache.GetAll<Faction>().Single(f => f.Name == "Pirates");
|
||||
Assert.Equal(key, check.Cache.RefOf(after).Key);
|
||||
Assert.Equal("authored by hand", after.Description);
|
||||
Assert.Equal(others, after.Allegiance.Count);
|
||||
Assert.Equal(1f, after.Allegiance[lateRef]);
|
||||
Assert.All(after.Allegiance.Values, value => Assert.Equal(1f, value));
|
||||
}
|
||||
finally { check.Cache.Dispose(); }
|
||||
|
||||
var settled = File.ReadAllBytes(CatalogPath);
|
||||
Assert.NotEqual(topped, settled);
|
||||
Assert.Equal(0, Program.PiratesFaction(apply: true, root: _root));
|
||||
Assert.Equal(settled, File.ReadAllBytes(CatalogPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -45,12 +45,14 @@ public sealed class ProductLinesCatalogTests
|
|||
Assert.Empty(shared);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WeaponLineRowsAreShipped()
|
||||
[Theory]
|
||||
[InlineData("weapons", 20)]
|
||||
[InlineData("minor-powers", 10)]
|
||||
public void LineRowsAreShipped(string line, int rows)
|
||||
{
|
||||
using var cache = RestoredHullsTests.OpenCatalog();
|
||||
var table = ProductLinesCommand.Table("weapons");
|
||||
Assert.Equal(20, table.Count);
|
||||
var table = ProductLinesCommand.Table(line);
|
||||
Assert.Equal(rows, table.Count);
|
||||
foreach (var row in table)
|
||||
{
|
||||
var product = Assert.Single(cache.GetAll<FactionProductData>(), p => p.Name == row.Name);
|
||||
|
|
|
|||
122
tools/AetherDb/MinorPowerFactionsCommand.cs
Normal file
122
tools/AetherDb/MinorPowerFactionsCommand.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
|
||||
// minor-power-factions [apply] (aetheria-release, cut minor-power-factions; rulings no-territory-ban, miss-terris-belongs-to-emily-r3):
|
||||
// authors the Aya Collective, Megiddo and Orbital Forge as ordinary factions, and the allegiance every existing faction but the
|
||||
// Pirates and Miss Terri's holds to them. The Pirates' allegiance is pirates-faction's alone (it tops itself up on a re-run).
|
||||
// A faction already in the catalog under its name-derived key is skipped, as is a gain already held, so the same command is the
|
||||
// first apply, the second apply and the replay onto a merged catalog. Dry run unless passed "apply".
|
||||
public static class MinorPowerFactionsCommand
|
||||
{
|
||||
// Own: the weights this faction names for the other minor powers; every other faction but the Pirates gets Default.
|
||||
// Gain: the weight every existing faction (bar the Pirates and Miss Terri's) gains toward this faction.
|
||||
// Geonames is the name file the faction draws place names from (question minor-power-geonames: the nearest existing file).
|
||||
internal sealed record Def(string Name, string ShortName, string Description, float3 Primary, float3 Secondary,
|
||||
string Geonames, int Influence, float Gain, float Default, (string ShortName, float Weight)[] Own);
|
||||
|
||||
internal static readonly Def[] Factions =
|
||||
{
|
||||
new("Aya Collective", "Aya",
|
||||
"A federation of communes, clinics and repair crews. Its equipment comes with the drawings, so the people it protects can keep it running.",
|
||||
new float3(.72f, .36f, .22f), new float3(.12f, .36f, .25f), "Congo", 3, .3f, .3f,
|
||||
new[] { ("Forge", 1f), ("Megiddo", .5f) }),
|
||||
new("Megiddo", "Megiddo",
|
||||
"A defended fleet that holds its boundary until the question is answered. Others buy its protection without sharing its faith.",
|
||||
new float3(.07f, .09f, .15f), new float3(.90f, .85f, .74f), "pleiades", 4, .3f, .2f,
|
||||
new[] { ("Aya", .5f), ("Forge", .5f) }),
|
||||
new("Orbital Forge", "Forge",
|
||||
"Belt engineers who hollow asteroids into homes and yards. Their standards leave room to adapt them, and their drawings travel further than their ships.",
|
||||
new float3(.42f, .30f, .22f), new float3(.30f, .38f, .52f), "pleiades", 3, .5f, .5f,
|
||||
Array.Empty<(string, float)>()),
|
||||
};
|
||||
|
||||
private const string Pirates = "Pirates";
|
||||
private const string MissTerris = "Miss Terri's";
|
||||
|
||||
// A faction's record key is derived from its name, so a replay reproduces the same key and the same bytes.
|
||||
internal static CultRecordKey KeyOf(Def def) =>
|
||||
new(new Guid(SHA256.HashData(Encoding.UTF8.GetBytes("faction:" + def.Name)).AsSpan(0, 16)).ToString("N"));
|
||||
|
||||
public static int Run(string[] args, string root = null)
|
||||
{
|
||||
var apply = args.Contains("apply");
|
||||
var db = AetherDb.Open(catalogWritable: apply, root: root);
|
||||
var existing = db.Cache.GetAll<Faction>().ToArray();
|
||||
var nameFiles = db.Cache.GetAll<NameFile>().ToArray();
|
||||
|
||||
// Refuse before any write: a name or short name held by a faction under another key, and a missing name file.
|
||||
foreach (var def in Factions)
|
||||
{
|
||||
var key = KeyOf(def);
|
||||
foreach (var other in existing.Where(f => f.Name == def.Name || f.ShortName == def.ShortName))
|
||||
if (!db.Cache.RefOf(other).Key.Equals(key))
|
||||
throw new InvalidOperationException("Faction Name or ShortName: held by a faction under another key (conflict).");
|
||||
if (nameFiles.Count(n => n.Name == def.Geonames) != 1)
|
||||
throw new InvalidOperationException("Faction GeonameFile: expected exactly one name file (missing).");
|
||||
}
|
||||
|
||||
var minorKeys = Factions.ToDictionary(d => d.ShortName, KeyOf);
|
||||
var created = Factions.Where(d => !existing.Any(f => db.Cache.RefOf(f).Key.Equals(KeyOf(d)))).ToArray();
|
||||
|
||||
// Every existing faction that is not a minor power, the Pirates or Miss Terri's holds each minor power it lacks.
|
||||
var holders = existing.Where(f => f.ShortName != Pirates && f.ShortName != MissTerris && !minorKeys.ContainsKey(f.ShortName)).ToArray();
|
||||
var gains = holders
|
||||
.Select(h => (Holder: h, Missing: Factions.Where(d => !h.Allegiance.ContainsKey(new CultRecordRef<Faction>(minorKeys[d.ShortName]))).ToArray()))
|
||||
.Where(g => g.Missing.Length != 0).ToArray();
|
||||
|
||||
if (created.Length == 0 && gains.Length == 0)
|
||||
{
|
||||
Console.WriteLine("The minor powers are already authored; nothing to do.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var def in created)
|
||||
Console.WriteLine($"New faction \"{def.Name}\" ({def.ShortName}): influence {def.Influence}, names \"{def.Geonames}\", colours ({def.Primary.x:0.00}, {def.Primary.y:0.00}, {def.Primary.z:0.00}) / ({def.Secondary.x:0.00}, {def.Secondary.y:0.00}, {def.Secondary.z:0.00})\n {def.Description}\n allegiance: {string.Join(", ", def.Own.Select(o => $"{o.ShortName} {o.Weight:0.##}"))} others {def.Default:0.##}");
|
||||
foreach (var (holder, missing) in gains)
|
||||
Console.WriteLine($" {holder.ShortName} gains {string.Join(", ", missing.Select(d => $"{d.ShortName} {d.Gain:0.##}"))}");
|
||||
|
||||
if (!apply)
|
||||
{
|
||||
Console.WriteLine($"\nDry run. Pass \"apply\" to author {created.Length} factions and {gains.Sum(g => g.Missing.Length)} allegiance gains.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A new faction names every faction but the Pirates, the other minor powers included.
|
||||
var everyone = existing.Where(f => f.ShortName != Pirates && !minorKeys.ContainsKey(f.ShortName)).Select(f => db.Cache.RefOf(f)).ToArray();
|
||||
if (!db.Cache.Commit(batch =>
|
||||
{
|
||||
foreach (var def in created) batch.Expect(KeyOf(def), null);
|
||||
foreach (var def in created)
|
||||
{
|
||||
var faction = new Faction
|
||||
{
|
||||
Name = def.Name, ShortName = def.ShortName, Description = def.Description,
|
||||
PrimaryColor = def.Primary, SecondaryColor = def.Secondary,
|
||||
GeonameFile = db.Cache.RefOf(nameFiles.Single(n => n.Name == def.Geonames)), InfluenceDistance = def.Influence,
|
||||
};
|
||||
foreach (var reference in everyone) faction.Allegiance[reference] = def.Default;
|
||||
foreach (var other in Factions.Where(o => o != def))
|
||||
faction.Allegiance[new CultRecordRef<Faction>(minorKeys[other.ShortName])] =
|
||||
def.Own.Where(o => o.ShortName == other.ShortName).Select(o => o.Weight).DefaultIfEmpty(def.Default).First();
|
||||
batch.Upsert(typeof(Faction), faction, KeyOf(def));
|
||||
}
|
||||
foreach (var (holder, missing) in gains)
|
||||
{
|
||||
foreach (var def in missing) holder.Allegiance[new CultRecordRef<Faction>(minorKeys[def.ShortName])] = def.Gain;
|
||||
batch.Upsert(typeof(Faction), holder, db.Cache.RefOf(holder).Key);
|
||||
}
|
||||
}))
|
||||
throw new InvalidOperationException("The catalog changed while the minor powers were being authored; nothing was written. Run it again.");
|
||||
Console.WriteLine($"\nAuthored {created.Length} factions and {gains.Sum(g => g.Missing.Length)} allegiance gains in Aetheria.cc");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -44,8 +44,23 @@ public static class ProductLinesCommand
|
|||
new("Mine Launcher", "Tripwire", "Zhestokost", "Closes the road behind the column."),
|
||||
};
|
||||
|
||||
// The Aya Collective, Megiddo and Orbital Forge (cut minor-power-factions): ordinary makers, applied after minor-power-factions.
|
||||
private static readonly Row[] MinorPowers =
|
||||
{
|
||||
new("Point Defense Gun", "Common Guard", "Aya", "Standard mount, removable feed box, a barrel you can swap at the bench. Drawings included."),
|
||||
new("Point Defense Laser", "Porch Light", "Aya", "Keeps the dark off the doorstep. The manual is longer than the warranty, on purpose."),
|
||||
new("Flak Gun", "Canopy", "Aya", "Shade for the whole convoy. Every part replaceable by the people it covers."),
|
||||
new("Point Defense Gun", "Watchman", "Megiddo", "Holds the line it is given. Every round accounted for."),
|
||||
new("Point Defense Laser", "Vigil", "Megiddo", "It does not sleep while the boundary stands."),
|
||||
new("Flak Gun", "Perimeter", "Megiddo", "Nothing crosses that we have not seen."),
|
||||
new("Light Machine Gun", "Pattern 12", "Forge", "Drawings ship with the gun. Build the next one yourself."),
|
||||
new("Autocannon", "Pattern 30", "Forge", "Standard bore, standard feed, standard anywhere a Belt yard can cut steel."),
|
||||
new("Slug Rifle", "Pattern 6", "Forge", "Licensed or not, it fits the same mount."),
|
||||
new("Point Defense Gun", "Pattern 2", "Forge", "Close-in defence to an open standard. No license check, ever."),
|
||||
};
|
||||
|
||||
// Every line the command knows, by the word that follows "product-lines".
|
||||
private static readonly Dictionary<string, Row[]> Lines = new() { ["weapons"] = Weapons };
|
||||
private static readonly Dictionary<string, Row[]> Lines = new() { ["weapons"] = Weapons, ["minor-powers"] = MinorPowers };
|
||||
|
||||
// A line's rows, for the catalog tests that check the shipped records against the table the command authors from.
|
||||
internal static IReadOnlyList<Row> Table(string line) => Lines[line];
|
||||
|
|
|
|||
|
|
@ -56,9 +56,10 @@ public static class Program
|
|||
case "generic-designs": return GenericDesigns(args.Contains("apply"));
|
||||
case "inspection-scanner": return InspectionScannerCatalog(args.Contains("apply"));
|
||||
case "product-lines": return ProductLinesCommand.Run(args.Skip(1).ToArray());
|
||||
case "minor-power-factions": return MinorPowerFactionsCommand.Run(args.Skip(1).ToArray());
|
||||
case "doctrine-catalog": return DoctrineCatalog.Run(AetherDb.Open(catalogWritable: args.Contains("apply")).Cache, args.Contains("apply"), Console.Out);
|
||||
default:
|
||||
Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], range-falloff [apply] [--effective <fraction>] [--end <multiplier>], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], smoke-hull [apply], smoke-rounds [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], launch-suppressor [apply], pirates-faction [apply], verse-grammar [apply], generic-designs [apply], sensor-stat-set [apply], inspection-scanner [apply], product-lines weapons [apply], doctrine-catalog [apply]");
|
||||
Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], range-falloff [apply] [--effective <fraction>] [--end <multiplier>], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], smoke-hull [apply], smoke-rounds [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], launch-suppressor [apply], pirates-faction [apply], verse-grammar [apply], generic-designs [apply], sensor-stat-set [apply], inspection-scanner [apply], minor-power-factions [apply], product-lines weapons|minor-powers [apply], doctrine-catalog [apply]");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -258,6 +259,7 @@ public static class Program
|
|||
["Zhestokost"] = (.50f, .08f), ["AU"] = (.50f, .12f), ["Lightsail"] = (.55f, .10f), ["R&D"] = (.60f, .15f),
|
||||
["Lucent"] = (.60f, .15f), ["NiteLife"] = (.55f, .12f), ["Finch"] = (.65f, .20f), ["Alakrita"] = (.60f, .14f),
|
||||
["DME"] = (.55f, .20f), ["Adrasteia"] = (.60f, .10f),
|
||||
["Aya"] = (.50f, .08f), ["Megiddo"] = (.75f, .05f), ["Forge"] = (.45f, .15f),
|
||||
};
|
||||
|
||||
private static readonly (float Mean, float Dev) PirateProfile = (.45f, .22f);
|
||||
|
|
@ -2259,6 +2261,11 @@ public static class Program
|
|||
// only, written here as \U escapes so no editor or shell mangles them; the dry run prints them raw and as code points.
|
||||
//
|
||||
// Dry run unless passed "apply", same contract as the other *-migrate commands.
|
||||
// The factions the Pirates do not yet hold allegiance to: every faction but themselves and those already named. The one
|
||||
// rule of both the first apply and the top-up, so they cannot diverge.
|
||||
private static Faction[] AllegianceGaps(AetherDb db, Faction pirates, IEnumerable<Faction> factions) =>
|
||||
factions.Where(f => !ReferenceEquals(f, pirates) && !pirates.Allegiance.ContainsKey(db.Cache.RefOf(f))).ToArray();
|
||||
|
||||
public static int PiratesFaction(bool apply, string root = null)
|
||||
{
|
||||
var db = AetherDb.Open(catalogWritable: apply, root: root);
|
||||
|
|
@ -2266,7 +2273,23 @@ public static class Program
|
|||
var existing = db.Cache.GetAll<Faction>().FirstOrDefault(f => f.Name == "Pirates");
|
||||
if (existing != null)
|
||||
{
|
||||
Console.WriteLine("A faction named \"Pirates\" already exists; nothing to do.");
|
||||
// A Pirates record already in the catalog is topped up, never rewritten: it gains allegiance 1.0 to each faction added
|
||||
// since, under its existing key, and nothing else changes.
|
||||
var gaps = AllegianceGaps(db, existing, db.Cache.GetAll<Faction>());
|
||||
if (gaps.Length == 0)
|
||||
{
|
||||
Console.WriteLine("The Pirates' allegiance already covers every faction; nothing to do.");
|
||||
return 0;
|
||||
}
|
||||
Console.WriteLine($"The Pirates lack allegiance to {gaps.Length} faction(s): {string.Join(", ", gaps.Select(f => f.Name))}.");
|
||||
if (!apply)
|
||||
{
|
||||
Console.WriteLine("\nDry run. Pass \"apply\" to give them allegiance 1.0 to each.");
|
||||
return 0;
|
||||
}
|
||||
foreach (var gap in gaps) existing.Allegiance[db.Cache.RefOf(gap)] = 1f;
|
||||
db.Cache.Commit(batch => batch.Upsert(typeof(Faction), existing, db.Cache.RefOf(existing).Key));
|
||||
Console.WriteLine($"\nGave the Pirates allegiance 1.0 to {gaps.Length} faction(s) in Aetheria.cc");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -2293,7 +2316,7 @@ public static class Program
|
|||
GeonameFile = db.Cache.RefOf(nameFile),
|
||||
InfluenceDistance = 3,
|
||||
};
|
||||
foreach (var other in others) pirates.Allegiance[db.Cache.RefOf(other)] = 1f;
|
||||
foreach (var other in AllegianceGaps(db, pirates, others)) pirates.Allegiance[db.Cache.RefOf(other)] = 1f;
|
||||
|
||||
var rebrands = new (string Design, string Name, string Description)[]
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue