A bad package station kind excludes the package; AetherDb opens a catalog with a kindless station hull, names it, and station-kinds hull sets it; the raw opener is gone

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
soul 2026-10-23 01:21:04 +02:00
commit d2f4b2c347
5 changed files with 59 additions and 19 deletions

View file

@ -14,10 +14,12 @@ public static class AetheriaStores
// Attaches (hydrates) the catalog read-only unless catalogWritable, then the run and player stores when given.
// Throws when a [CultGlobal] type routed to the catalog has no record. The one exemption is a writable catalog with
// no records at all, a seed store being authored from nothing; a populated catalog is held to it however it opens.
// A catalog's station hull with no Kind is refused, unless the caller gives kindlessStation: the tool that repairs a catalog
// is told the hull's name and goes on.
// The registry is the process-wide one unless the caller scopes it (the test host does, so its own document types never
// become catalog globals); the global check above is judged against whichever registry opened the cache.
public static CultCache Open(string catalogPath, string runPath = null, string playerPath = null, bool catalogWritable = false,
CultDocumentRegistry registry = null)
CultDocumentRegistry registry = null, Action<string> kindlessStation = null)
{
// A read-only open of an absent catalog would hydrate an empty game; a shipped player missing its catalog stops here.
if (!catalogWritable && !File.Exists(catalogPath))
@ -42,7 +44,7 @@ public static class AetheriaStores
foreach (var data in cache.GetAll<ConsumableItemData>()) CultRecordRefs.Validate(data);
VerseGrammarValidation.ValidateAll(cache.GetAll<VerseVerb>());
ValidateBins(cache);
StationKinds.Validate(cache);
StationKinds.Validate(cache, kindlessStation);
return cache;
}
catch

View file

@ -108,6 +108,7 @@ public static class ShipModCatalog
Exclude(directory, $"hull name '{hull.Name}' collides with an existing catalog item.");
if (shipIds.Contains(ship.Id))
Exclude(directory, $"ship ID collides with an existing catalog ship authoring record.");
if (StationKinds.HullKindFault(cache, hull) is { } kindFault) Exclude(directory, kindFault);
foreach (var product in package.Products)
{
if (!factionKeys.Contains(product.Manufacturer.Key))

View file

@ -3,25 +3,27 @@ using System.IO;
using GameCult.Caching;
// Opens GameData/Aetheria.cc through AetheriaStores, the game's own store composition. The catalog attaches read-only
// unless a command writes it, so a command that only reads cannot write.
// unless a command writes it, so a command that only reads cannot write. A station hull with no kind does not lock the
// catalog: it is named to warn (stderr by default) and the commands that repair it stay reachable.
public sealed class AetherDb
{
public string Root { get; }
public CultCache Cache { get; }
private AetherDb(string root, bool catalogWritable, bool withRun)
private AetherDb(string root, bool catalogWritable, bool withRun, Action<string> warn)
{
Root = root;
var gameData = Path.Combine(root, "GameData");
Cache = AetheriaStores.Open(
Path.Combine(gameData, "Aetheria.cc"),
runPath: withRun ? Path.Combine(gameData, "run.cc") : null,
catalogWritable: catalogWritable);
catalogWritable: catalogWritable,
kindlessStation: warn ?? Console.Error.WriteLine);
}
// With no root given, walks up from the working directory to the game folder: the first folder whose GameData holds Aetheria.cc
public static AetherDb Open(bool catalogWritable = false, bool withRun = false, string root = null) =>
new AetherDb(root ?? FindRoot(), catalogWritable, withRun);
public static AetherDb Open(bool catalogWritable = false, bool withRun = false, string root = null, Action<string> warn = null) =>
new AetherDb(root ?? FindRoot(), catalogWritable, withRun, warn);
public static string FindRoot()
{

View file

@ -61,10 +61,23 @@ public static class Program
case "place-names":
Console.OutputEncoding = new System.Text.UTF8Encoding(false);
return PlaceNamesCommand.Run(args.Skip(1).ToArray());
case "station-kinds": return StationKindsCatalog.Run(StationKindsCatalog.OpenCatalog(args.Contains("apply")), args.Contains("apply"), Console.Out);
case "station-kinds":
{
var apply = args.Contains("apply");
using var db = AetherDb.Open(catalogWritable: apply);
int code;
if (args.Length > 1 && args[1] == "hull")
{
if (args.Length != (apply ? 5 : 4)) { Console.WriteLine("station-kinds hull \"<hull name>\" <kind> [apply]"); return 1; }
code = StationKindsCatalog.SetHullKind(db.Cache, args[2], args[3], apply, Console.Out);
}
else code = StationKindsCatalog.Run(db.Cache, apply, Console.Out);
if (apply) db.Cache.FlushAsync().GetAwaiter().GetResult();
return code;
}
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], minor-power-factions [apply], place-names [apply], product-lines weapons|minor-powers|gear|tiers [apply], doctrine-catalog [apply], assembly-from-maker [apply], station-kinds [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], place-names [apply], product-lines weapons|minor-powers|gear|tiers [apply], doctrine-catalog [apply], assembly-from-maker [apply], station-kinds [hull \"<name>\" <kind>] [apply]");
return 1;
}
}

View file

@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Linq;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
// Authors the catalog's one list of station kinds (aetheria-living-world, cut station-kind-datum) and gives Zenith, the
// one station hull in the repo, its ruled kind (ruling zenith-is-a-trade-hub: provisional, one edit here to change).
@ -34,15 +33,6 @@ public static class StationKindsCatalog
("waystation", "Waystation", "Fuel, repair and medicine along a corridor."),
};
// The catalog opened without AetheriaStores.Open's checks: the catalog this command first runs on holds a station hull with no
// kind, which StationKinds.Validate refuses, and this command is what gives it one.
public static CultCache OpenCatalog(bool apply, string catalogPath = null)
{
var cache = new CultCache();
cache.AddBackingStore(new SingleFileMessagePackBackingStore(catalogPath ?? Path.Combine(AetherDb.FindRoot(), "GameData", "Aetheria.cc"), !apply), AetheriaStores.CatalogTypes);
return cache;
}
public static int Run(CultCache cache, bool apply, TextWriter output)
{
var held = cache.GetAll<StationKindData>().Select(kind => cache.RefOf(kind).Key.Value).ToHashSet(StringComparer.Ordinal);
@ -55,6 +45,9 @@ public static class StationKindsCatalog
output.WriteLine(zenith == null ? "Zenith: no such hull, kind not set"
: setZenith ? $"Zenith: kind {ZenithKind}" : "Zenith: already has a kind, kept");
foreach (var hull in cache.GetAll<HullData>().Where(h => h.HullType == HullType.Station && !h.Kind.IsSet() && h != zenith).OrderBy(h => h.Name, StringComparer.Ordinal))
output.WriteLine($"Station hull '{hull.Name}' has no kind; set one with: station-kinds hull \"{hull.Name}\" <kind> apply");
if (!apply)
{
output.WriteLine("\nDry run. Pass \"apply\" to author them.");
@ -79,4 +72,33 @@ public static class StationKindsCatalog
output.WriteLine($"\nAuthored {missing.Length} station kinds{(setZenith ? " and Zenith's kind" : "")} in Aetheria.cc");
return 0;
}
// Gives a station hull its kind: the way to repair a hull the catalog holds without one. Dry run unless apply.
public static int SetHullKind(CultCache cache, string hullName, string kindId, bool apply, TextWriter output)
{
var hull = cache.GetAll<HullData>().FirstOrDefault(h => h.HullType == HullType.Station && h.Name == hullName);
if (hull == null)
{
output.WriteLine($"The catalog holds no station hull named '{hullName}'.");
return 1;
}
var kind = new CultRecordRef<StationKindData>(StationKindData.KeyOf(kindId));
if (cache.Get(kind) == null)
{
output.WriteLine($"The catalog holds no station kind '{kindId}'.");
return 1;
}
output.WriteLine($"{hull.Name}: kind {kindId}");
if (!apply)
{
output.WriteLine("
Dry run. Pass \"apply\" to set it.");
return 0;
}
hull.Kind = kind;
cache.Upsert(hull);
output.WriteLine($"
Set {hull.Name}'s kind in Aetheria.cc");
return 0;
}
}