Prefilter loadouts with typed catalog rows

This commit is contained in:
Meta Vens 2026-06-13 23:20:16 +01:00
commit 1806ba221e
4 changed files with 80 additions and 20 deletions

Binary file not shown.

View file

@ -73,6 +73,9 @@ abstraction has been deleted; item data projection is routed through
`Projection` is not durable state; it is a bridge for surviving simulation and
UI code. The remaining single-item bridge is named `GetRuntimeItemProjection<T>`,
not catalog entry access.
Loadout generation now applies typed catalog prefilters for item category,
hardpoint type, shape fit, hull type, and behavior kind before hydrating legacy
`ItemData` projections for final simulation compatibility.
Surviving item catalog Unity DTOs inherit `RuntimeItemProjectionEntry`; the old
`RuntimeCatalogEntry` base name no longer appears in live source. Temporary
faction/corporation projections and zone body/orbit DTOs keep local GUID fields

View file

@ -93,7 +93,9 @@ public class LoadoutGenerator
var emptyShape = entity.UnoccupiedSpace;
var dockingBayData = RandomItem<DockingBayData>(2, item => item.Shape.FitsWithin(emptyShape, out _, out _));
var dockingBayData = RandomItem<DockingBayData>(2,
item => item.Shape.FitsWithin(emptyShape, out _, out _),
item => FitsWithin(item, emptyShape));
if (dockingBayData == null) throw new InvalidLoadoutException("No compatible docking bay found for station!");
dockingBayData.Shape.FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition);
@ -107,8 +109,10 @@ public class LoadoutGenerator
OutfitEntity(entity);
var cargo = entity.CargoBays.First();
IEnumerable<EquippableItemData> inventory = RandomItems<EquippableItemData>(16, 1,
data => !(data is HullData hull && hull.HullType != HullType.Ship) && !(data is CargoBayData));
IEnumerable<EquippableItemData> inventory = RandomItems<EquippableItemData>(16, 1,
data => !(data is HullData hull && hull.HullType != HullType.Ship) && !(data is CargoBayData),
item => item.Category != "CargoBayData" && item.Category != "DockingBayData" &&
(item.Category != "HullData" || item.HullType == nameof(HullType.Ship)));
inventory = inventory
.OrderByDescending(item=>item.Shape.Coordinates.Length);
foreach (var item in inventory)
@ -129,7 +133,11 @@ public class LoadoutGenerator
item.HullType == type);
}
public T[] RandomItems<T>(int count, float sizeExponent, Predicate<T> filter = null) where T : EquippableItemData
public T[] RandomItems<T>(
int count,
float sizeExponent,
Predicate<T> filter = null,
Predicate<AetheriaRuntimeCatalogItem> typedFilter = null) where T : EquippableItemData
{
return RuntimeCatalog.EquipmentItems
.Where(IsTypedCandidate<T>)
@ -138,7 +146,8 @@ public class LoadoutGenerator
Guid.TryParse(item.ManufacturerLegacyId, out var manufacturer) &&
manufacturer != Guid.Empty &&
(Galaxy.IsPrelude || Galaxy.ContainsFaction(manufacturer) &&
(Faction == null || Faction.Allegiance.ContainsKey(manufacturer))))
(Faction == null || Faction.Allegiance.ContainsKey(manufacturer))) &&
(typedFilter?.Invoke(item) ?? true))
.Select(ProjectRuntimeItem<T>)
.Where(item => item != null)
.Where(item =>
@ -198,17 +207,51 @@ public class LoadoutGenerator
: 1;
}
public T RandomItem<T>(float sizeExponent, Predicate<T> filter = null) where T : EquippableItemData
public T RandomItem<T>(
float sizeExponent,
Predicate<T> filter = null,
Predicate<AetheriaRuntimeCatalogItem> typedFilter = null) where T : EquippableItemData
{
return RandomItems(1, sizeExponent, filter).FirstOrDefault();
return RandomItems(1, sizeExponent, filter, typedFilter).FirstOrDefault();
}
public T RandomItem<T>(HardpointData hardpoint, float sizeExponent, Predicate<T> filter = null) where T : EquippableItemData
{
return RandomItem<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);
return RandomItem<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,
item => FitsHardpoint(item, hardpoint));
}
private static bool FitsHardpoint(AetheriaRuntimeCatalogItem item, HardpointData hardpoint)
{
return string.Equals(item.HardpointType, hardpoint.Type.ToString(), StringComparison.Ordinal) &&
item.OccupiedCells == hardpoint.Shape.Coordinates.Length &&
FitsWithin(item, hardpoint.Shape, hardpoint.Rotation);
}
private static bool FitsWithin(AetheriaRuntimeCatalogItem item, Shape target)
{
return ToShape(item.ShapeWidth, item.ShapeHeight, item.ShapeCells).FitsWithin(target, out _, out _);
}
private static bool FitsWithin(AetheriaRuntimeCatalogItem item, Shape target, ItemRotation rotation)
{
return ToShape(item.ShapeWidth, item.ShapeHeight, item.ShapeCells).FitsWithin(target, rotation, out _);
}
private static Shape ToShape(int width, int height, IReadOnlyList<AetheriaRuntimeShapeCell> cells)
{
var shape = new Shape(Math.Max(width, 1), Math.Max(height, 1));
foreach (var cell in cells)
{
shape[int2(cell.X, cell.Y)] = true;
}
return shape;
}
private void OutfitEntity(Entity entity)
@ -256,9 +299,10 @@ public class LoadoutGenerator
var emptyShape = entity.UnoccupiedSpace;
var cargoData = RandomItem<CargoBayData>(3, item =>
!(item is DockingBayData) &&
item.Shape.FitsWithin(emptyShape, out _, out _));
var cargoData = RandomItem<CargoBayData>(3,
item => !(item is DockingBayData) &&
item.Shape.FitsWithin(emptyShape, out _, out _),
item => item.Category != "DockingBayData" && FitsWithin(item, emptyShape));
if (cargoData == null) throw new InvalidLoadoutException("No compatible cargo bay found for entity!");
cargoData.Shape.FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition);
@ -269,9 +313,11 @@ public class LoadoutGenerator
emptyShape = entity.UnoccupiedSpace;
var capacitorData = RandomItem<GearData>(2, item =>
item.Behaviors.Any(b => b is CapacitorData) &&
item.Shape.FitsWithin(emptyShape, out _, out _));
var capacitorData = RandomItem<GearData>(2,
item => item.Behaviors.Any(b => b is CapacitorData) &&
item.Shape.FitsWithin(emptyShape, out _, out _),
item => item.BehaviorKinds.Contains(nameof(CapacitorData), StringComparer.Ordinal) &&
FitsWithin(item, emptyShape));
if (capacitorData == null) throw new InvalidLoadoutException("No compatible capacitor found for entity!");
capacitorData.Shape.FitsWithin(emptyShape, out var capacitorRotation, out var capacitorPosition);

View file

@ -122,7 +122,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
commands, not in-client catalog hydration shortcuts. `LoadoutGenerator` also
receives the typed runtime catalog and uses it to own
item candidate selection before hydrating selected legacy item DTOs by ID for
exact fitting checks and behavior construction. The unused `TradeMenuDebug`
remaining behavior construction and legacy simulation checks. Hardpoint type,
shape fit, station bay fit, hull/category, and behavior-kind prefilters now
run against typed catalog rows before any DTO projection is hydrated. The unused `TradeMenuDebug`
script has been deleted instead of preserving an old uGUI debug path that
turned typed trade rows back into legacy `ItemData` objects. That hydration
now comes from typed state, not `AetherDB.msgpack`.
@ -391,7 +393,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
marked with project-owned `LegacyPayloadKeyAttribute`, not MessagePack
metadata. Item properties
manufacturer display is a typed snapshot consumer; loadout generation is the
remaining runtime single-ID item projection bridge. The old trade debug UI
remaining runtime single-ID item projection bridge, but typed catalog rows own
the loadout candidate prefilters for shape, category, hardpoint type, and
behavior kind before that bridge is invoked. The old trade debug UI
and console `give` command have been deleted. No live caller can enumerate
legacy catalog entries or request catalog-shaped single entries through
`ItemManager`.
@ -558,6 +562,10 @@ First Aetheria surfaces to publish:
- Done: move `LoadoutGenerator` item candidate selection to the typed
runtime catalog; legacy item DTO hydration remains only for exact fitting
checks and behavior construction.
- Done: move loadout hardpoint fit, station bay fit, hull/category, and
behavior-kind prefilters ahead of DTO hydration so typed catalog rows
reject incompatible candidates before `GetRuntimeItemProjection<T>` is
called.
- Done: delete unused `TradeMenuDebug`; the old debug uGUI trade path no
longer hydrates typed trade rows back into legacy `ItemData` objects.
- Done: delete the console `give` command instead of preserving a debug
@ -802,4 +810,7 @@ temporary `AetheriaRuntimeItemCatalog` DTO materializer. The typed state spine
and migration quarantine exist; the next cuts should replace surviving
projection consumers with typed item facets, typed behavior factories, and Eve
surfaces until `GetRuntimeItemProjection<T>` has no caller and the old DTO
bridge can be deleted.
bridge can be deleted. Loadout generation should keep moving eligibility tests
onto `AetheriaRuntimeCatalogItem` rows before projection; any predicate that
still needs `ItemData` must earn that dependency by using behavior objects or
simulation-only methods that typed facets do not yet expose.