Scenarios content: delete five industrial placeholders; products for four weapons and the heater

Operator rulings, 2026-09-30 (docs/scenarios-cut.md, "the ten real designs with
no product"), landed through a transient `AetherDb product-gap [apply]`:

- Deleted, nothing referencing them: Assembly Line, Deep Ore Extractor, Surface
  Ore Extractor, Refinery, Shipyard (RTS-era placeholders, price and durability
  0, three with unreadable behaviour entries).
- Products, maker taken from the nearest analogue's product:
    Autocannon  -> Autocannon by Zhestokost (DeathCluster: Ballistic, 4 cells, AutoWeapon)
    LRMM72      -> LRMM72 by Rossum & Douglas (GT 3K: the only other unguided launcher)
    SRMM72      -> SRMM72 by Death Monkey Explosives (scorched void policy: guided, nearest size and price)
    Tractor Beam -> Space Tractor by Ewan Hart Inc (no analogue; the wishlist's
                   "Space Tractor: It's a tractor. In space.", content-batch-one section 10)
    Industrial Thermostatic Heater -> same name by Lightsail Express (Cockpit 2x2,
                   the same Thermotoggle/EnergyDraw/Heat group)
  Each product carries its design's roles at the default quality (0.5 +- 0.15).
- The heater design kept (operator), with Price 0 -> 100,000 (between Cockpit
  2x2 and Fire Control Array) and Durability 0 -> 100 (Cockpit 2x2's): at zero
  price generation never offers it, at zero durability it never comes online.

The rewrite stores every weapon record at the current schema, so
FireControlCut124's shipped-catalog check now expects an Exact migration
report instead of drift with slot 32 defaulted. `AetherDb census` reports no
unsold design. Tractor Beam keeps price 0, durability 0 and no behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 15:45:18 +02:00
commit 653ba490a7
3 changed files with 88 additions and 6 deletions

BIN
GameData/Aetheria.cc (Stored with Git LFS)

Binary file not shown.

View file

@ -2584,7 +2584,8 @@ public sealed class FireControlCut124Tests : IDisposable
}
// Every WeaponItemData in the shipped catalog has a null BlastRadius and a null Fuse, and the weapon
// schema's own migration report is CompatibleDrift with only slot 32 defaulted and no other slots touched.
// schema's own migration report is Exact. (Until the scenarios product-gap pass rewrote the catalog on
// 2026-09-30 it was CompatibleDrift with only slot 32 defaulted: the file predated BlastRadius and Fuse.)
[Fact]
public void ShippedCatalogIsNeutralOnBlastRadiusAndFuse()
{
@ -2604,8 +2605,8 @@ public sealed class FireControlCut124Tests : IDisposable
Assert.NotEmpty(reports);
foreach (var report in reports)
{
Assert.Equal(CultSchemaMigrationKind.CompatibleDrift, report.Kind);
Assert.Equal(new[] { 32 }, report.DefaultedMissingSlots);
Assert.Equal(CultSchemaMigrationKind.Exact, report.Kind);
Assert.Empty(report.DefaultedMissingSlots);
Assert.Empty(report.IgnoredExtraSlots);
}
}

View file

@ -36,8 +36,9 @@ public static class Program
case "targeting-catalog": return TargetingCatalog(args.Contains("apply"));
case "targeting-catalog-6c": return TargetingCatalog6c(args.Contains("apply"));
case "targeting-catalog-6d": return TargetingCatalog6d(args.Contains("apply"));
case "product-gap": return ProductGap(args.Contains("apply"));
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], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [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], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], product-gap [apply]");
return 1;
}
}
@ -1474,4 +1475,84 @@ public static class Program
Console.WriteLine($"\nLanded {precisionChanges.Length} Precision changes in Aetheria.cc");
return 0;
}
// Scenarios map, operator rulings 2026-09-30 ("the ten product-less designs"): after this, a design with no
// product is a Q3 scenario test design and nothing else. The five RTS-era industrial placeholders are deleted
// (nothing references them). The four weapons are a content gap and get a product each, from the maker of their
// nearest analogue: same hardpoint and firing behaviour, then nearest size. The Industrial Thermostatic Heater is
// generic gear every generated station carries; it gets a product from the maker of the cockpit whose thermostat
// it shares (Thermotoggle, EnergyDraw, Heat in one group), and the price and durability without which generation
// cannot offer it (RandomProducts skips a zero price) and it can never come online (zero durability).
private static int ProductGap(bool apply)
{
var db = AetherDb.Open(catalogWritable: apply);
var cache = db.Cache;
T Design<T>(string name) where T : EquippableItemData =>
cache.GetAll<T>().SingleOrDefault(d => d.Name == name) ?? throw new InvalidOperationException($"No design named \"{name}\".");
Faction MakerOf(string productName) =>
cache.Get(cache.GetAll<FactionProductData>().Single(p => p.Name == productName).Manufacturer);
Faction Named(string factionName) => cache.GetAll<Faction>().Single(f => f.Name == factionName);
bool HasProduct(EquippableItemData design) =>
cache.GetAll<FactionProductData>().Any(p => p.Design.Key.Equals(cache.RefOf(design).Key));
var industrial = new[] { "Assembly Line", "Deep Ore Extractor", "Surface Ore Extractor", "Refinery", "Shipyard" }
.Select(Design<EquippableItemData>).ToArray();
foreach (var design in industrial)
if (HasProduct(design)) throw new InvalidOperationException($"{design.Name} has a product; it is not a placeholder.");
var heater = Design<GearData>("Industrial Thermostatic Heater");
var products = new (EquippableItemData Design, string Name, string Description, Faction Maker, string Analogue)[]
{
(Design<EquippableItemData>("Autocannon"), "Autocannon", "", MakerOf("DeathCluster"),
"DeathCluster: Ballistic, 4 cells, AutoWeapon"),
(Design<EquippableItemData>("LRMM72"), "LRMM72", "", MakerOf("GT 3K"),
"GT 3K: the only other unguided LauncherData"),
(Design<EquippableItemData>("SRMM72"), "SRMM72", "", MakerOf("scorched void policy"),
"scorched void policy: GuidedWeapon launcher, nearest in size and price"),
(Design<EquippableItemData>("Tractor Beam"), "Space Tractor", "It's a tractor. In space.", Named("Ewan Hart Inc"),
"none (no behaviours); Ewan Hart's wishlist Space Tractor, docs/content-batch-one.md section 10"),
(heater, "Industrial Thermostatic Heater", "", MakerOf("Cockpit 2x2"),
"Cockpit 2x2: the same thermostat behaviours"),
};
foreach (var (design, _, _, _, _) in products)
if (HasProduct(design)) throw new InvalidOperationException($"{design.Name} already has a product.");
const int heaterPrice = 100000; // between Cockpit 2x2 (50,000) and Fire Control Array (150,000)
const float heaterDurability = 100; // Cockpit 2x2's
Console.WriteLine("Delete (no product, no reference):");
foreach (var design in industrial) Console.WriteLine($" {design.Name}");
Console.WriteLine("Products:");
foreach (var (design, name, _, maker, analogue) in products)
Console.WriteLine($" {design.Name,-32} -> {name,-32} by {maker.Name,-24} ({analogue})");
Console.WriteLine($"Heater design: Price {heater.Price} -> {heaterPrice}, Durability {heater.Durability} -> {heaterDurability}");
if (!apply)
{
Console.WriteLine("\nDry run. Pass \"apply\" to land it.");
return 0;
}
heater.Price = heaterPrice;
heater.Durability = heaterDurability;
CultRecordRefs.Validate(heater);
var industrialKeys = industrial.Select(design => cache.RefOf(design).Key).ToArray();
var heaterKey = cache.RefOf(heater).Key;
var made = products.Select(entry => new FactionProductData
{
Name = entry.Name,
Description = entry.Description,
Design = new CultRecordRef<CraftedItemData>(cache.RefOf(entry.Design).Key),
Manufacturer = cache.RefOf(entry.Maker),
Roles = (entry.Design.Roles ?? new List<ItemRole>()).Select(role => new ProductRole { Role = role.Name }).ToList()
}).ToArray();
cache.Commit(batch =>
{
foreach (var key in industrialKeys) batch.Remove(key);
batch.Upsert(typeof(GearData), heater, heaterKey);
foreach (var product in made) batch.Upsert(typeof(FactionProductData), product, new CultRecordKey(Guid.NewGuid().ToString("N")));
});
Console.WriteLine($"\nLanded {industrial.Length} deletions, {made.Length} products and the heater's price and durability in Aetheria.cc");
return 0;
}
}