From fe7ea72c9b4a36e64943e2082125992aa5001c4a Mon Sep 17 00:00:00 2001 From: soul Date: Fri, 23 Oct 2026 17:20:04 +0200 Subject: [PATCH] StationKinds.Choose: a kind the faction places but no maker can field leaves its pool and falls back to any maker A stand-in whose hull kind no offered station hull carries (Zhestokost's depot, whose model has not landed) used to leave the tutorial entrance with no station. The existing no-kinds fallback now covers it: one rule, no faction special case. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/ServerShared/StationKinds.cs | 29 ++++++++++++------- .../Aetheria.Shared.Tests/StationKindTests.cs | 28 ++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/Assets/Scripts/ServerShared/StationKinds.cs b/Assets/Scripts/ServerShared/StationKinds.cs index 4d7c54ffe..d7ac014b4 100644 --- a/Assets/Scripts/ServerShared/StationKinds.cs +++ b/Assets/Scripts/ServerShared/StationKinds.cs @@ -52,22 +52,21 @@ public static class StationKinds return Ordered(kinds); } - // A station's kind: a stable pick among the faction's kinds by the station's site, which generation names by the zone's - // seed and the station's place in its orbit order (never by a key the cache mints). It draws nothing from any random - // stream, so adding the kind to generation moves no other roll. A faction with no kinds falls back to any kind some - // offered station hull carries, logged, and the hull draw then falls back to any maker. Unset when the catalog has none. + // A station's kind: a stable pick among the faction's kinds it can field, by the station's site, which generation names by the + // zone's seed and the station's place in its orbit order (never by a key the cache mints). It draws nothing from any random + // stream, so adding the kind to generation moves no other roll. A kind the faction places but cannot field (neither its own + // model nor a stand-in's hull kind is carried by any offered station hull) is not in the pool; a faction left with no kind to + // field falls back to any kind some offered station hull carries, logged, and the hull draw then falls back to any maker. + // Unset when the catalog has none. public static CultRecordRef Choose(CultCache catalog, Faction faction, string site, Action log = null) { - var pool = KindsOf(catalog, faction); + var offered = OfferedKinds(catalog); + var pool = KindsOf(catalog, faction).Where(kind => offered.Contains(HullKindFor(catalog, faction, kind).Key.Value)).ToArray(); if (pool.Length == 0) { - pool = Ordered(new HashSet( - catalog.GetAll().Where(product => LoadoutGenerator.IsOffered(catalog, product)) - .Select(product => catalog.Get(product.Design) as HullData) - .Where(hull => hull != null && hull.HullType == HullType.Station && hull.Kind.IsSet()) - .Select(hull => hull.Kind.Key.Value), StringComparer.Ordinal)); + pool = Ordered(offered); if (pool.Length == 0) return default; - log?.Invoke($"{faction?.Name ?? "This galaxy"} builds no station kind; placing a station of any kind some maker offers."); + log?.Invoke($"{faction?.Name ?? "This galaxy"} builds no station kind it can field; placing a station of any kind some maker offers."); } return pool[(int) (site.StableHash() % (uint) pool.Length)]; } @@ -130,6 +129,14 @@ public static class StationKinds } } + // The hull kinds some maker offers: the pool a station can always be drawn from. + private static HashSet OfferedKinds(CultCache catalog) => + new HashSet( + catalog.GetAll().Where(product => LoadoutGenerator.IsOffered(catalog, product)) + .Select(product => catalog.Get(product.Design) as HullData) + .Where(hull => hull != null && hull.HullType == HullType.Station && hull.Kind.IsSet()) + .Select(hull => hull.Kind.Key.Value), StringComparer.Ordinal); + private static IEnumerable OfferedStationHulls(CultCache catalog, Faction faction) => catalog.GetAll() .Where(product => LoadoutGenerator.IsOffered(catalog, product) && catalog.Get(product.Manufacturer) == faction) diff --git a/tests/Aetheria.Shared.Tests/StationKindTests.cs b/tests/Aetheria.Shared.Tests/StationKindTests.cs index 47154c1c7..23d72e4e7 100644 --- a/tests/Aetheria.Shared.Tests/StationKindTests.cs +++ b/tests/Aetheria.Shared.Tests/StationKindTests.cs @@ -273,6 +273,34 @@ public sealed class StationKindTests : IDisposable } } + // A faction that places a kind through a stand-in no maker offers cannot field it: the kind is out of its pool and it falls back + // exactly as a faction with no kinds does. Once any maker offers the stand-in's hull kind, the faction keeps its kind. + [Fact] + public void FactionWhoseStandInNoMakerOffersFallsBackToAnyMaker() + { + var world = World.Make(this, zenithOffered: false); + world.Sell(world.Au, "shipyard"); + world.StandIn("mining", world.Lucent, "depot"); + Assert.Equal(new[] { Kind("mining").Key }, StationKinds.KindsOf(world.Cache, world.Lucent).Select(kind => kind.Key)); + + var logged = new List(); + var kind = StationKinds.Choose(world.Cache, world.Lucent, "site", logged.Add); + Assert.Equal(Kind("shipyard").Key, kind.Key); + Assert.Contains(world.Lucent.Name, Assert.Single(logged)); + var pack = world.Generator(world.Lucent).GenerateStationLoadout(kind); + Assert.NotNull(pack); + Assert.Equal(Kind("shipyard").Key, world.HullOf(pack).Kind.Key); + + world.Sell(world.Au, "depot"); + var kept = new List(); + var placed = StationKinds.Choose(world.Cache, world.Lucent, "site", kept.Add); + Assert.Equal(Kind("mining").Key, placed.Key); + Assert.Empty(kept); + var depotStation = world.Generator(world.Lucent).GenerateStationLoadout(placed); + Assert.NotNull(depotStation); + Assert.Equal(Kind("depot").Key, world.HullOf(depotStation).Kind.Key); + } + [Fact] public void FactionWithoutModelsFallsBack() {