Use typed loadout candidate selectors

This commit is contained in:
Meta Vens 2026-06-14 09:31:17 +01:00
commit 2a871e4cfb
3 changed files with 64 additions and 41 deletions

Binary file not shown.

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using GameCult.Aetheria.State.Unity;
using Unity.Mathematics;
@ -9,6 +8,21 @@ using Random = Unity.Mathematics.Random;
public class LoadoutGenerator
{
private enum RuntimeItemCandidateKind
{
Equipment,
Gear,
CargoBay,
DockingBay,
Hull
}
private const string GearCategory = "GearData";
private const string WeaponCategory = "WeaponItemData";
private const string CargoBayCategory = "CargoBayData";
private const string DockingBayCategory = "DockingBayData";
private const string HullCategory = "HullData";
public Random Random;
public ItemManager ItemManager { get; }
public AetheriaRuntimeCatalogSnapshot RuntimeCatalog { get; }
@ -93,7 +107,7 @@ public class LoadoutGenerator
var emptyShape = entity.UnoccupiedSpace;
var dockingBayRow = RandomCatalogItem<DockingBayData>(2, item => FitsWithin(item, emptyShape));
var dockingBayRow = RandomCatalogItem(RuntimeItemCandidateKind.DockingBay, 2, item => FitsWithin(item, emptyShape));
if (dockingBayRow == null) throw new InvalidLoadoutException("No compatible docking bay found for station!");
ToShape(dockingBayRow).FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition);
@ -107,9 +121,9 @@ public class LoadoutGenerator
OutfitEntity(entity);
var cargo = entity.CargoBays.First();
IEnumerable<AetheriaRuntimeCatalogItem> inventory = RandomCatalogItems<EquippableItemData>(16, 1,
item => item.Category != "CargoBayData" && item.Category != "DockingBayData" &&
(item.Category != "HullData" || item.HullType == nameof(HullType.Ship)));
IEnumerable<AetheriaRuntimeCatalogItem> inventory = RandomCatalogItems(RuntimeItemCandidateKind.Equipment, 16, 1,
item => item.Category != CargoBayCategory && item.Category != DockingBayCategory &&
(item.Category != HullCategory || item.HullType == nameof(HullType.Ship)));
inventory = inventory
.Where(item => item != null);
inventory = inventory
@ -127,18 +141,19 @@ public class LoadoutGenerator
public AetheriaRuntimeCatalogItem RandomHull(HullType type, Predicate<AetheriaRuntimeCatalogItem> hullFilter = null)
{
return RandomCatalogItem<HullData>(0, item =>
return RandomCatalogItem(RuntimeItemCandidateKind.Hull, 0, item =>
string.Equals(item.HullType, type.ToString(), StringComparison.Ordinal) &&
(hullFilter?.Invoke(item) ?? true));
}
public AetheriaRuntimeCatalogItem[] RandomCatalogItems<T>(
private AetheriaRuntimeCatalogItem[] RandomCatalogItems(
RuntimeItemCandidateKind candidateKind,
int count,
float sizeExponent,
Predicate<AetheriaRuntimeCatalogItem> typedFilter = null) where T : EquippableItemData
Predicate<AetheriaRuntimeCatalogItem> typedFilter = null)
{
return RuntimeCatalog.EquipmentItems
.Where(IsTypedCandidate<T>)
.Where(item => IsTypedCandidate(item, candidateKind))
.Where(item =>
item.Price > 0 &&
Guid.TryParse(item.ManufacturerLegacyId, out var manufacturer) &&
@ -168,25 +183,23 @@ public class LoadoutGenerator
);
}
private static bool IsTypedCandidate<T>(AetheriaRuntimeCatalogItem item) where T : EquippableItemData
private static bool IsTypedCandidate(AetheriaRuntimeCatalogItem item, RuntimeItemCandidateKind candidateKind)
{
var requestedType = typeof(T);
if (requestedType == typeof(EquippableItemData))
switch (candidateKind)
{
return !string.IsNullOrWhiteSpace(item.HardpointType);
case RuntimeItemCandidateKind.Equipment:
return !string.IsNullOrWhiteSpace(item.HardpointType);
case RuntimeItemCandidateKind.Gear:
return item.Category == GearCategory || item.Category == WeaponCategory;
case RuntimeItemCandidateKind.CargoBay:
return item.Category == CargoBayCategory || item.Category == DockingBayCategory;
case RuntimeItemCandidateKind.DockingBay:
return item.Category == DockingBayCategory;
case RuntimeItemCandidateKind.Hull:
return item.Category == HullCategory;
default:
throw new ArgumentOutOfRangeException(nameof(candidateKind), candidateKind, null);
}
if (requestedType == typeof(GearData))
{
return item.Category == "GearData" || item.Category == "WeaponItemData";
}
if (requestedType == typeof(CargoBayData))
{
return item.Category == "CargoBayData" || item.Category == "DockingBayData";
}
return string.Equals(item.Category, requestedType.Name, StringComparison.Ordinal);
}
private float ManufacturerDistancePenalty(Guid manufacturer)
@ -202,16 +215,22 @@ public class LoadoutGenerator
: 1;
}
public AetheriaRuntimeCatalogItem RandomCatalogItem<T>(
private AetheriaRuntimeCatalogItem RandomCatalogItem(
RuntimeItemCandidateKind candidateKind,
float sizeExponent,
Predicate<AetheriaRuntimeCatalogItem> typedFilter = null) where T : EquippableItemData
Predicate<AetheriaRuntimeCatalogItem> typedFilter = null)
{
return RandomCatalogItems<T>(1, sizeExponent, typedFilter).FirstOrDefault();
return RandomCatalogItems(candidateKind, 1, sizeExponent, typedFilter).FirstOrDefault();
}
public AetheriaRuntimeCatalogItem RandomCatalogItem<T>(AetheriaRuntimeHardpoint hardpoint, float sizeExponent, Predicate<AetheriaRuntimeCatalogItem> filter = null) where T : EquippableItemData
private AetheriaRuntimeCatalogItem RandomCatalogItem(
RuntimeItemCandidateKind candidateKind,
AetheriaRuntimeHardpoint hardpoint,
float sizeExponent,
Predicate<AetheriaRuntimeCatalogItem> filter = null)
{
return RandomCatalogItem<T>(
return RandomCatalogItem(
candidateKind,
sizeExponent,
item => FitsHardpoint(item, hardpoint) && (filter?.Invoke(item) ?? true));
}
@ -308,7 +327,7 @@ public class LoadoutGenerator
: entity is OrbitalEntity
? nameof(TurretControllerData)
: null;
var controllerRow = RandomCatalogItem<GearData>(hardpoint, 2, item => HasBehaviorKind(item, controllerBehaviorKind));
var controllerRow = RandomCatalogItem(RuntimeItemCandidateKind.Gear, hardpoint, 2, item => HasBehaviorKind(item, controllerBehaviorKind));
if (controllerRow == null)
throw new InvalidLoadoutException("No compatible controller found for entity!");
var controller = ItemManager.CreateEquippableInstance(controllerRow);
@ -326,7 +345,7 @@ public class LoadoutGenerator
: Guid.TryParse(itemRow.LegacyId, out var previousItemId)
? entity.Equipment.FirstOrDefault(item => item.EquippableItem.Data.ItemId == previousItemId)
: null;
itemRow ??= RandomCatalogItem<GearData>(hardpoint, 2);
itemRow ??= RandomCatalogItem(RuntimeItemCandidateKind.Gear, hardpoint, 2);
if (itemRow == null) ItemManager.Log($"No compatible item found for entity {hardpoint.Type} hardpoint!");
else
{
@ -346,7 +365,7 @@ public class LoadoutGenerator
var emptyShape = entity.UnoccupiedSpace;
var cargoRow = RandomCatalogItem<CargoBayData>(3, item => item.Category != "DockingBayData" && FitsWithin(item, emptyShape));
var cargoRow = RandomCatalogItem(RuntimeItemCandidateKind.CargoBay, 3, item => item.Category != DockingBayCategory && FitsWithin(item, emptyShape));
if (cargoRow == null) throw new InvalidLoadoutException("No compatible cargo bay found for entity!");
ToShape(cargoRow).FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition);
@ -357,7 +376,7 @@ public class LoadoutGenerator
emptyShape = entity.UnoccupiedSpace;
var capacitorRow = RandomCatalogItem<GearData>(2,
var capacitorRow = RandomCatalogItem(RuntimeItemCandidateKind.Gear, 2,
item => item.BehaviorKinds.Contains(nameof(CapacitorData), StringComparer.Ordinal) &&
FitsWithin(item, emptyShape));
if (capacitorRow == null) throw new InvalidLoadoutException("No compatible capacitor found for entity!");

View file

@ -155,11 +155,11 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
corporation projection. There is no live console item-spawn command; future
operator item actions should be typed command documents or Eve/CultUI
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
remaining behavior construction and legacy simulation checks. Hull type,
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`
receives the typed runtime catalog and uses typed candidate-kind selectors
for item selection and instantiation. Hull type, hardpoint type, shape fit,
station bay fit, hull/category, and behavior-kind prefilters run against typed
catalog rows; the remaining behavior bridge builds temporary `BehaviorData`
config from typed payloads only at behavior-constructor boundaries. 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`. The surviving trade menu
@ -705,8 +705,12 @@ First Aetheria surfaces to publish:
- Done: move `LoadoutGenerator` weighted candidate selection, hardpoint fit,
station bay fit, cargo/capacitor fit, selected-item reuse, hull/category,
hull type, hull conductivity setup, and behavior-kind prefilters onto typed
runtime catalog rows; selected legacy DTO projection remains only as the
item-instantiation bridge after a typed row wins.
runtime catalog rows; selected item instantiation now receives typed rows
directly.
- Done: replace `LoadoutGenerator`'s legacy DTO generic selectors with
private typed candidate-kind selectors, so loadout code no longer asks for
`HullData`, `GearData`, `CargoBayData`, or `EquippableItemData` as
candidate-selection authority.
- Done: delete unused `TradeMenuDebug`; the old debug uGUI trade path no
longer hydrates typed trade rows back into legacy `ItemData` objects.
- Done: move surviving trade menu size, hardpoint, and behavior filters onto