AetherDb: product-lines command, with the weapons table
One command file owns the twenty second-maker weapon products; roles and quality come from Program.MakerProfiles, now internal. A maker already selling the design is not refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d267a535d2
commit
fa3c2ab4d3
2 changed files with 134 additions and 3 deletions
130
tools/AetherDb/ProductLinesCommand.cs
Normal file
130
tools/AetherDb/ProductLinesCommand.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/* 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 GameCult.Caching;
|
||||
|
||||
// product-lines <line> [apply] (aetheria-release, cut product-lines-weapons; rulings catalog-grows-generic-designs-branded-products,
|
||||
// maker-design-tiers-allowed, miss-terris-belongs-to-emily-r3, pirates-record-content): authors the branded products of one
|
||||
// product line into GameData/Aetheria.cc. A line is a table of (design, product, maker, text); each row becomes one
|
||||
// FactionProductData with one ProductRole per role of its design, at the maker's profile in Program.MakerProfiles, so the table
|
||||
// carries no quality numbers. A row already in the catalog (same name, design and maker) is skipped, so the same command is the
|
||||
// first apply, the second apply and the replay onto a merged catalog. A maker that already sells the design is not refused: a
|
||||
// tier is allowed. Dry run unless passed "apply".
|
||||
public static class ProductLinesCommand
|
||||
{
|
||||
internal sealed record Row(string Design, string Name, string Maker, string Description);
|
||||
|
||||
private static readonly Row[] Weapons =
|
||||
{
|
||||
new("Light Machine Gun", "Rattler", "AU", "Cheap, loud, and there when the rocks start moving."),
|
||||
new("Slug Rifle", "SM-6 Marksman", "R&D", "Standardised, certified, guaranteed to hit the target.*"),
|
||||
new("Long Slug Rifle", "Longshot", "Finch", "Light, quiet and dead accurate, when it feels like it."),
|
||||
new("Autocannon", "Workhorse", "AU", "Feeds from anything, fires at anything."),
|
||||
new("Cluster Cannon", "everything at once", "DME", "why aim/ when you can be everywhere"),
|
||||
new("Charged Scattergun", "Grapeshot", "Zhestokost", "The old answer to boarders, sized for the void."),
|
||||
new("Light Laser", "Sparrow", "Finch", "Barely registers on the scale, or on their sensors."),
|
||||
new("Light Laser", "Spotlight", "Lucent", "Make sure they know who got them."),
|
||||
new("Cell Blaster", "Strobe", "Lucent", "Fast, bright, and very hard to ignore."),
|
||||
new("Long Laser", "Merlin", "Finch", "Hovers at the edge of range, then stoops."),
|
||||
new("Heavy Beam Laser", "Afterglow", "NiteLife", "Keeps glowing long after the fight is over."),
|
||||
new("Charged Blaster", "Encore", "Lucent", "One more, for the fans."),
|
||||
new("Charged Scatter Blaster", "nobody leaves early", "DME", "doors are locked/ lights are low/ stay for the finale"),
|
||||
new("Lightning Projector", "Static Shock", "NiteLife", "A jolt for every ship in reach."),
|
||||
new("Guided Missile Launcher", "Homer", "Finch", "Finds its way, mostly."),
|
||||
new("Long-Range Missile Rack", "Long Haul", "AU", "Delivers to the far side of the zone."),
|
||||
new("Short-Range Missile Rack", "Short Fuse", "AU", "For problems already too close."),
|
||||
new("Swarm Rocket Pod", "Confetti", "Lucent", "Every launch is a celebration."),
|
||||
new("Incendiary Rocket Pod", "Trebuchet", "Zhestokost", "Siege fire for a column that does not stop."),
|
||||
new("Mine Launcher", "Tripwire", "Zhestokost", "Closes the road behind the column."),
|
||||
};
|
||||
|
||||
// Every line the command knows, by the word that follows "product-lines".
|
||||
private static readonly Dictionary<string, Row[]> Lines = new() { ["weapons"] = Weapons };
|
||||
|
||||
// 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];
|
||||
|
||||
public static int Run(string[] args, string root = null)
|
||||
{
|
||||
var apply = args.Contains("apply");
|
||||
var line = args.FirstOrDefault(a => a != "apply");
|
||||
if (line == null || !Lines.TryGetValue(line, out var rows))
|
||||
{
|
||||
Console.WriteLine($"usage: product-lines {string.Join("|", Lines.Keys)} [apply]");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var db = AetherDb.Open(catalogWritable: apply, root: root);
|
||||
var designs = db.Cache.GetAll<CraftedItemData>().ToArray();
|
||||
var factions = db.Cache.GetAll<Faction>().ToArray();
|
||||
var products = db.Cache.GetAll<FactionProductData>().ToArray();
|
||||
|
||||
// Resolve every row before anything is written: a missing or ambiguous design or maker, a name that belongs to another
|
||||
// product or to a design, and a design with no roles each stop the command with the catalog untouched.
|
||||
var resolved = new List<(Row Row, CraftedItemData Design, Faction Maker, bool Present)>();
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var design = designs.Where(d => d.Name == row.Design).ToArray();
|
||||
if (design.Length != 1) throw new InvalidOperationException($"Expected exactly one design named \"{row.Design}\", found {design.Length}.");
|
||||
var maker = factions.Where(f => f.ShortName == row.Maker).ToArray();
|
||||
if (maker.Length != 1) throw new InvalidOperationException($"Expected exactly one faction with short name \"{row.Maker}\", found {maker.Length}.");
|
||||
if (!Program.MakerProfiles.ContainsKey(row.Maker)) throw new InvalidOperationException($"No maker profile for \"{row.Maker}\".");
|
||||
if (design[0].Roles.Count == 0) throw new InvalidOperationException($"The design \"{row.Design}\" has no roles.");
|
||||
|
||||
var designKey = db.Cache.RefOf(design[0]).Key;
|
||||
var makerKey = db.Cache.RefOf(maker[0]).Key;
|
||||
var named = products.Where(p => string.Equals(p.Name, row.Name, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
var present = named.Length == 1 && named[0].Name == row.Name && named[0].Design.Key.Equals(designKey) && named[0].Manufacturer.Key.Equals(makerKey);
|
||||
if (named.Length != 0 && !present) throw new InvalidOperationException($"The product name \"{row.Name}\" already belongs to another product.");
|
||||
if (!present && designs.Any(d => string.Equals(d.Name, row.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new InvalidOperationException($"The product name \"{row.Name}\" is the name of a design.");
|
||||
resolved.Add((row, design[0], maker[0], present));
|
||||
}
|
||||
|
||||
var missing = resolved.Where(r => !r.Present).ToArray();
|
||||
if (missing.Length == 0)
|
||||
{
|
||||
Console.WriteLine($"The {line} product line is already authored; nothing to do.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Product line {line}:");
|
||||
foreach (var (row, design, _, present) in resolved)
|
||||
{
|
||||
var (mean, dev) = Program.MakerProfiles[row.Maker];
|
||||
Console.WriteLine($" {(present ? "present" : "new ")} {row.Design} -> \"{row.Name}\" by {row.Maker}, mean {mean:0.##} dev {dev:0.##} on [{string.Join(", ", design.Roles.Select(r => r.Name))}]: {row.Description}");
|
||||
}
|
||||
|
||||
if (!apply)
|
||||
{
|
||||
Console.WriteLine($"\nDry run. Pass \"apply\" to author {missing.Length} products.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ExpectUnchanged lands the batch onto the file as it is: every other record keeps the bytes it has, where a plain
|
||||
// commit would re-encode the whole catalog through today's types.
|
||||
if (!db.Cache.Commit(batch =>
|
||||
{
|
||||
batch.ExpectUnchanged();
|
||||
foreach (var (row, design, maker, _) in missing)
|
||||
{
|
||||
var (mean, dev) = Program.MakerProfiles[row.Maker];
|
||||
batch.Upsert(typeof(FactionProductData), new FactionProductData
|
||||
{
|
||||
Name = row.Name,
|
||||
Description = row.Description,
|
||||
Design = db.Cache.RefOf(design),
|
||||
Manufacturer = db.Cache.RefOf(maker),
|
||||
Roles = design.Roles.Select(r => new ProductRole { Role = r.Name, Mean = mean, StandardDeviation = dev }).ToList(),
|
||||
});
|
||||
}
|
||||
}))
|
||||
throw new InvalidOperationException("The catalog changed while the product line was being authored; nothing was written. Run it again.");
|
||||
Console.WriteLine($"\nAuthored {missing.Length} products in Aetheria.cc");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -55,8 +55,9 @@ public static class Program
|
|||
case "sensor-stat-set": return SensorStatSet(args.Contains("apply"));
|
||||
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());
|
||||
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]");
|
||||
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]");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -250,8 +251,8 @@ public static class Program
|
|||
};
|
||||
|
||||
// One Mean / StandardDeviation per maker, applied to every role of a design the maker sells (catalog-growth
|
||||
// addenda, "Maker profiles"); the Pirates are middling and inconsistent (pirates-record).
|
||||
private static readonly Dictionary<string, (float Mean, float Dev)> MakerProfiles = new Dictionary<string, (float Mean, float Dev)>
|
||||
// addenda, "Maker profiles"); the Pirates are middling and inconsistent (pirates-record). Read by ProductLinesCommand too.
|
||||
internal static readonly Dictionary<string, (float Mean, float Dev)> MakerProfiles = new Dictionary<string, (float Mean, float Dev)>
|
||||
{
|
||||
["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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue