minor-power-factions command, MakerProfiles rows, Pirates allegiance top-up (ported from 57083412)

MinorPowerFactionsCommand authors Aya Collective, Megiddo and Orbital Forge as
ordinary factions plus the gains on other factions' Allegiance. pirates-faction on
an existing record now tops up allegiance under its key. Unbuilt here; verified at
the head on Yggdrasil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-17 15:54:31 +02:00
commit ac0b39f84e
2 changed files with 150 additions and 3 deletions

View 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;
}
}

View file

@ -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,25 @@ 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("
Dry 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($"
Gave the Pirates allegiance 1.0 to {gaps.Length} faction(s) in Aetheria.cc");
return 0;
}
@ -2293,7 +2318,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)[]
{