Generation never offers gear no hull can mount

Operator ruling, 2026-09-30, on the Autocannon: "Leave the autocannon unsold,
we'll need to author a bunch more hulls before all the gear variety in the game
has a home." The fit rule gets one owner, HardpointData.Takes (the gear's type,
laid in at the hardpoint's rotation, filling every cell); LoadoutGenerator's
hardpoint picks, its reuse of a previous product and `AetherDb hardpoint-fit`
all read it. RandomProducts, which both station stock and NPC loadouts draw
through, offers only gear with a home: Tool gear, hulls, or hardpoint gear some
hull in the catalog takes. The Autocannon keeps its product and price.

Test: the Autocannon is never drawn in 2000 draws of its hardpoint type; after
a scratch hull whose hardpoint takes it is authored, it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 18:01:35 +02:00
commit d91ab8daf9
4 changed files with 42 additions and 9 deletions

View file

@ -567,6 +567,12 @@ public class HardpointData
return $"{Enum.GetName(typeof(HardpointType), Type)} Hardpoint {Rotation.Arrow()}";
}
// The one fit rule for hardpoint gear: its type, laid in at this hardpoint's rotation, filling every cell.
public bool Takes(EquippableItemData design) =>
design.HardpointType == Type &&
design.Shape.FitsWithin(Shape, Rotation, out _) &&
design.Shape.Coordinates.Length == Shape.Coordinates.Length;
[IgnoreMember]
public float3 TintColor
{

View file

@ -141,12 +141,14 @@ public class LoadoutGenerator
// lacks variety, so it is logged rather than hidden.
public (FactionProductData product, T design)[] RandomProducts<T>(int count, float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
{
var hulls = ItemManager.ItemData.GetAll<HullData>().ToArray();
var candidates = ItemManager.ItemData.GetAll<FactionProductData>()
.Select(product => (product, design: ItemManager.ItemData.Get(product.Design) as T))
.Where(entry =>
entry.design != null &&
entry.design.Price > 0 &&
entry.product.Manufacturer.IsSet() &&
HasHome(entry.design, hulls) &&
(filter?.Invoke(entry.design) ?? true))
.ToArray();
var available = candidates.Where(entry => IsAvailable(entry.product)).ToArray();
@ -165,6 +167,13 @@ public class LoadoutGenerator
count);
}
// Hardpoint gear that no hull in the catalog can mount has no home (operator, 2026-09-30: "we'll need to author a
// bunch more hulls before all the gear variety in the game has a home"): generation never offers it, so no station
// stocks it. It gains one the moment a hull with a hardpoint that takes it exists. Tool gear goes in any interior.
private static bool HasHome(EquippableItemData design, HullData[] hulls) =>
design.HardpointType == HardpointType.Tool || design.HardpointType == HardpointType.Hull ||
hulls.Any(hull => hull.Hardpoints.Any(hardpoint => hardpoint.Takes(design)));
// 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
@ -192,10 +201,7 @@ public class LoadoutGenerator
public (FactionProductData product, T design) RandomProduct<T>(HardpointData hardpoint, float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
{
return RandomProduct<T>(sizeExponent, item => item.HardpointType == hardpoint.Type &&
(filter?.Invoke(item) ?? true) &&
item.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _) &&
item.Shape.Coordinates.Length==hardpoint.Shape.Coordinates.Length, required);
return RandomProduct<T>(sizeExponent, item => hardpoint.Takes(item) && (filter?.Invoke(item) ?? true), required);
}
private void OutfitEntity(Entity entity)
@ -227,7 +233,7 @@ public class LoadoutGenerator
{
// If a previously selected product fits, use that one (this is why we must process larger hardpoints first)
var entry = previousProducts
.FirstOrDefault(e => e.design.HardpointType == hardpoint.Type && e.design.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _));
.FirstOrDefault(e => hardpoint.Takes(e.design));
var previousItem = entity.Equipment.FirstOrDefault(item => item.Data == entry.design);
if (entry.design == null) entry = RandomProduct<GearData>(hardpoint, 2);
if (entry.design == null) ItemManager.Log($"No compatible item found for entity {Enum.GetName(typeof(HardpointType), hardpoint.Type)} hardpoint!");