From 9b7a1c00dc2b7034a16a61c436463008f4fa9996 Mon Sep 17 00:00:00 2001
From: Meta Vens
Date: Sun, 18 Oct 2026 18:11:11 +0200
Subject: [PATCH] Catalog: the six tier bins (product-lines tiers apply);
TierRowsAreShipped; the tier step pinned from both sides
Aetheria.cc written only by the command, on the pinned CultLib. Replay the command at merges; never merge the blob.
Co-Authored-By: Claude Sonnet 5.5
---
GameData/Aetheria.cc | 4 +-
tests/Aetheria.Shared.Tests/LoadoutTests.cs | 7 +++
.../ProductLinesCatalogTests.cs | 59 +++++++++++++++++++
3 files changed, 68 insertions(+), 2 deletions(-)
diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc
index 9f9ff69e5..a2ddaad5d 100644
--- a/GameData/Aetheria.cc
+++ b/GameData/Aetheria.cc
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:22342ea360c7f453de500da8650e6a23648f8c1250eb22e724765bbcf619eac0
-size 186037
+oid sha256:c327ef1bbabbd34ddb7b9f643f5db8cc54aa5730f232bd42a1fa5f2b03973e89
+size 189753
diff --git a/tests/Aetheria.Shared.Tests/LoadoutTests.cs b/tests/Aetheria.Shared.Tests/LoadoutTests.cs
index 5673b205d..86bf4e698 100644
--- a/tests/Aetheria.Shared.Tests/LoadoutTests.cs
+++ b/tests/Aetheria.Shared.Tests/LoadoutTests.cs
@@ -622,6 +622,13 @@ public sealed class LoadoutTests : IDisposable
var result = items.GetTier((CraftedItemInstance) items.CreateInstance(lot));
Assert.Equal(settings.Tiers[0].Name, result.tier.Name);
Assert.Equal(2, result.upgrades);
+
+ // The same width from the other side (follow-up tier-upgrade-step-two-sided): two steps less a hair is one upgrade, so a
+ // step narrower than .0499 reads two and a wider one reads none.
+ var under = items.Lots.Add(new Lot { Design = cache.RefOf(lamp), Origin = new Attributed(), Quality = settings.Tiers[0].Quality + 2 * .0499f - .0001f, Roles = new List() });
+ var below = items.GetTier((CraftedItemInstance) items.CreateInstance(under));
+ Assert.Equal(settings.Tiers[0].Name, below.tier.Name);
+ Assert.Equal(1, below.upgrades);
}
// S7 (docs/stats-and-power-cut.md Cut 1 findings): a Quality term must raise the lot's quality to its own
diff --git a/tests/Aetheria.Shared.Tests/ProductLinesCatalogTests.cs b/tests/Aetheria.Shared.Tests/ProductLinesCatalogTests.cs
index 6a91bb457..7ce78218f 100644
--- a/tests/Aetheria.Shared.Tests/ProductLinesCatalogTests.cs
+++ b/tests/Aetheria.Shared.Tests/ProductLinesCatalogTests.cs
@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Caching;
+using GameCult.Caching.MessagePack;
+using MessagePack;
using Xunit;
// The weapon product lines the shipped catalog holds (campaign aetheria-release, cut product-lines-weapons; rulings
@@ -99,4 +101,61 @@ public sealed class ProductLinesCatalogTests
}
}
}
+ // Cut product-tiers: the six bins of the command's tier table are in the catalog, each a single product that names its line.
+ [Fact]
+ public void TierRowsAreShipped()
+ {
+ using var cache = RestoredHullsTests.OpenCatalog();
+ var table = ProductLinesCommand.TierTable();
+ Assert.Equal(6, table.Count);
+ foreach (var row in table)
+ {
+ var bin = Assert.Single(cache.GetAll(), p => p.Name == row.Name);
+ var line = Assert.Single(cache.GetAll(), p => p.Name == row.Line);
+ Assert.Equal(cache.RefOf(line).Key, bin.Bin.Line.Key);
+ Assert.Equal(line.Design.Key, bin.Design.Key);
+ Assert.Equal(line.Manufacturer.Key, bin.Manufacturer.Key);
+ Assert.Equal(row.Floor, bin.Bin.Floor);
+ Assert.Equal(row.Description, bin.Description);
+ Assert.Empty(bin.Roles);
+ Assert.Null(bin.Assembly);
+ }
+ }
+
+ // A bin bins an unbinned line of its own design and maker, and the catalog holds no bin the table does not author.
+ [Fact]
+ public void BinsBinAnUnbinnedLineOfTheirOwnDesignAndMaker()
+ {
+ using var cache = RestoredHullsTests.OpenCatalog();
+ var bins = cache.GetAll().Where(p => p.Bin != null).ToArray();
+ Assert.Equal(ProductLinesCommand.TierTable().Select(r => r.Name).OrderBy(n => n, StringComparer.Ordinal), bins.Select(b => b.Name).OrderBy(n => n, StringComparer.Ordinal));
+ foreach (var bin in bins)
+ {
+ var line = cache.Get(bin.Bin.Line);
+ Assert.NotNull(line);
+ Assert.Null(line.Bin);
+ Assert.Equal(line.Design.Key, bin.Design.Key);
+ Assert.Equal(line.Manufacturer.Key, bin.Manufacturer.Key);
+ Assert.True(bin.Bin.Floor >= 0 && float.IsFinite(bin.Bin.Floor));
+ }
+ }
+
+ // FactionProductData slot 7 is additive under the same schema id: a bin round-trips, and a record written before the slot
+ // existed (an array one element shorter) reads no bin.
+ [Fact]
+ public void BinIsSlotSevenAndACatalogWithoutItReadsNoBin()
+ {
+ var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(FactionProductData).Assembly);
+ var binned = MessagePackSerializer.Deserialize(
+ MessagePackSerializer.Serialize(new FactionProductData { Name = "Binned", Bin = new ProductBin { Floor = .67f } }, options), options);
+ Assert.Equal(.67f, binned.Bin.Floor);
+
+ var bytes = MessagePackSerializer.Serialize(new FactionProductData { Name = "Old", Description = "d" }, options);
+ Assert.Equal(0x98, bytes[0]);
+ Assert.Equal(0xC0, bytes[^1]);
+ var before = new byte[] { 0x97 }.Concat(bytes.Skip(1).Take(bytes.Length - 2)).ToArray();
+ var old = MessagePackSerializer.Deserialize(before, options);
+ Assert.Equal("Old", old.Name);
+ Assert.Null(old.Bin);
+ }
}