From d50fd1b9738313578b65de4e18860a27eeb41c97 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 23:13:34 +0200 Subject: [PATCH] A faction reaches its own gear; allegiance names only other factions Operator ruling, 2026-09-30: "removing the 11 self-entries was my intention". IsAvailable, the one reach rule, now says a faction always reaches its own manufacturer's gear, and a transient `AetherDb allegiance-self [apply]` drops the self-entries from the eleven factions that had one (Rossum & Douglas never did, which is why its stations could not carry its own gear). Tests: in a main-sector galaxy every faction is offered all it makes and no allegiance map names its own faction; and, through a fixture faction outside the galaxy with no self-entry, its own product is on offer to it. Co-Authored-By: Claude Opus 5.5 --- .../Scripts/ServerShared/LoadoutGenerator.cs | 5 ++- GameData/Aetheria.cc | 4 +-- tests/Aetheria.Shared.Tests/RunStartTests.cs | 20 +++++++++++ tools/AetherDb/Program.cs | 34 ++++++++++++++++++- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/ServerShared/LoadoutGenerator.cs b/Assets/Scripts/ServerShared/LoadoutGenerator.cs index 4fe94b054..2fb2a69f9 100644 --- a/Assets/Scripts/ServerShared/LoadoutGenerator.cs +++ b/Assets/Scripts/ServerShared/LoadoutGenerator.cs @@ -177,9 +177,12 @@ public class LoadoutGenerator // No galaxy means no availability to filter by: every product is on offer. A fixture generates loadouts that // way, so a test can exercise placement and products without standing up a whole galaxy; no game path does. // Loadouts.Materialize takes availability as a predicate; RunStart, the game's one preset spawner, passes this, so - // presets and generation share one availability rule. + // presets and generation share one availability rule. In a galaxy, a faction always reaches its own manufacturer's + // gear, and otherwise gear made by a manufacturer in that galaxy that its allegiance names (operator, 2026-09-30: + // allegiance lists only other factions). public bool IsAvailable(FactionProductData product) => Galaxy == null || Galaxy.IsPrelude || + Faction != null && ItemManager.ItemData.Get(product.Manufacturer) == Faction || Galaxy.ContainsFaction(product.Manufacturer) && (Faction == null || Faction.Allegiance.ContainsKey(product.Manufacturer)); // Prioritize products from the zone faction and its allies, penalizing distance to the manufacturer's headquarters diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index f247b3fd4..20377e0db 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ce919b3fbe9149cbbddd1cf0b9e6d93379c406770dfdd60928715ba4f564b6f -size 12702246 +oid sha256:70a7b0a9f5a59bf64a0bff80f5d266083910e88cc141745b5082424e3ae30b79 +size 12701773 diff --git a/tests/Aetheria.Shared.Tests/RunStartTests.cs b/tests/Aetheria.Shared.Tests/RunStartTests.cs index 75c490e3b..d0a888a3d 100644 --- a/tests/Aetheria.Shared.Tests/RunStartTests.cs +++ b/tests/Aetheria.Shared.Tests/RunStartTests.cs @@ -461,6 +461,22 @@ public sealed class RunStartTests : IDisposable } } + // A faction always reaches its own manufacturer's gear: in a main-sector galaxy every product a faction makes is + // on offer to that faction, allegiance or not. And an allegiance map lists only other factions. + [Fact] + public void AFactionReachesItsOwnGearAndItsAllegianceNamesOnlyOthers() + { + var galaxy = MainGalaxy(); + var products = _cache.GetAll().Where(p => p.Manufacturer.IsSet()).ToArray(); + foreach (var faction in galaxy.Factions) + { + Assert.DoesNotContain(faction.Allegiance.Keys, key => key.Key.Equals(_cache.RefOf(faction).Key)); + var generator = new LoadoutGenerator(ref _items.Random, _items, galaxy, galaxy.Entrance, faction, .5f); + var own = products.Where(p => p.Manufacturer.Key.Equals(_cache.RefOf(faction).Key)).ToList(); + Assert.True(own.All(generator.IsAvailable), $"{faction.Name} cannot reach its own {string.Join(", ", own.Where(p => !generator.IsAvailable(p)).Select(p => p.Name))}"); + } + } + // A faction with no access to Zhestokost (its allegiance names only AU, Lightsail and NiteLife) gets // stations with no Zhestokost gear, powered by a reactor it can reach, and an idle one of them holds its heater's // cells above freezing over the second five of ten minutes. @@ -477,6 +493,10 @@ public sealed class RunStartTests : IDisposable var products = _cache.GetAll().Where(p => p.Manufacturer.IsSet()).ToArray(); Assert.DoesNotContain(products.Where(p => p.Manufacturer.Key.Equals(_cache.RefOf(zhestokost).Key)), generator.IsAvailable); Assert.Contains(products.Where(p => p.Manufacturer.Key.Equals(_cache.RefOf(Named("AU")).Key)), generator.IsAvailable); + // Its allegiance does not name itself, yet it reaches what it makes. + var own = new FactionProductData { Name = "Cooperative Capacitor", Design = products.First().Design, Manufacturer = _cache.RefOf(coop) }; + _cache.Upsert(own); + Assert.True(generator.IsAvailable(own), "a faction reaches its own manufacturer's gear"); var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs Entity station = null; for (var i = 0; i < 8; i++) diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 1e0572798..e23ffe251 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -37,8 +37,9 @@ public static class Program case "targeting-catalog-6c": return TargetingCatalog6c(args.Contains("apply")); case "targeting-catalog-6d": return TargetingCatalog6d(args.Contains("apply")); case "station-reactors": return StationReactors(args.Contains("apply")); + case "allegiance-self": return AllegianceSelf(args.Contains("apply")); default: - Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear ]... [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], station-reactors [apply]"); + Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear ]... [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], station-reactors [apply], allegiance-self [apply]"); return 1; } } @@ -1556,4 +1557,35 @@ public static class Program Console.WriteLine($"\nLanded {changes.Count} records in Aetheria.cc"); return 0; } + + // Scenarios map, operator ruling 2026-09-30 ("removing the 11 self-entries was my intention"): a faction always + // reaches its own manufacturer's gear (LoadoutGenerator.IsAvailable), so an allegiance map lists only other + // factions. This drops each faction's entry for itself. + private static int AllegianceSelf(bool apply) + { + var db = AetherDb.Open(catalogWritable: apply); + var cache = db.Cache; + var changed = new List<(Faction Faction, CultRecordKey Key)>(); + foreach (var faction in cache.GetAll().OrderBy(f => f.Name, StringComparer.Ordinal)) + { + var self = cache.RefOf(faction); + var entry = faction.Allegiance.Keys.FirstOrDefault(k => k.Key.Equals(self.Key)); + if (!entry.IsSet()) continue; + Console.WriteLine($"{faction.Name}: drop its own allegiance entry ({faction.Allegiance[entry]})"); + faction.Allegiance.Remove(entry); + changed.Add((faction, self.Key)); + } + + if (!apply) + { + Console.WriteLine($"\nDry run. Pass \"apply\" to land {changed.Count} changes."); + return 0; + } + cache.Commit(batch => + { + foreach (var (faction, key) in changed) batch.Upsert(typeof(Faction), faction, key); + }); + Console.WriteLine($"\nLanded {changed.Count} changes in Aetheria.cc"); + return 0; + } }