diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index d66b06bc6..fe8895e1d 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Aetheria.State/README.md b/Aetheria.State/README.md index 3ba473615..a4eb3e353 100644 --- a/Aetheria.State/README.md +++ b/Aetheria.State/README.md @@ -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`, 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 diff --git a/Assets/Scripts/ServerShared/LoadoutGenerator.cs b/Assets/Scripts/ServerShared/LoadoutGenerator.cs index de559dee7..ecd079481 100644 --- a/Assets/Scripts/ServerShared/LoadoutGenerator.cs +++ b/Assets/Scripts/ServerShared/LoadoutGenerator.cs @@ -93,7 +93,9 @@ public class LoadoutGenerator var emptyShape = entity.UnoccupiedSpace; - var dockingBayData = RandomItem(2, item => item.Shape.FitsWithin(emptyShape, out _, out _)); + var dockingBayData = RandomItem(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 inventory = RandomItems(16, 1, - data => !(data is HullData hull && hull.HullType != HullType.Ship) && !(data is CargoBayData)); + IEnumerable inventory = RandomItems(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(int count, float sizeExponent, Predicate filter = null) where T : EquippableItemData + public T[] RandomItems( + int count, + float sizeExponent, + Predicate filter = null, + Predicate typedFilter = null) where T : EquippableItemData { return RuntimeCatalog.EquipmentItems .Where(IsTypedCandidate) @@ -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) .Where(item => item != null) .Where(item => @@ -198,17 +207,51 @@ public class LoadoutGenerator : 1; } - public T RandomItem(float sizeExponent, Predicate filter = null) where T : EquippableItemData + public T RandomItem( + float sizeExponent, + Predicate filter = null, + Predicate typedFilter = null) where T : EquippableItemData { - return RandomItems(1, sizeExponent, filter).FirstOrDefault(); + return RandomItems(1, sizeExponent, filter, typedFilter).FirstOrDefault(); } public T RandomItem(HardpointData hardpoint, float sizeExponent, Predicate filter = null) where T : EquippableItemData { - return RandomItem(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( + 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 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(3, item => - !(item is DockingBayData) && - item.Shape.FitsWithin(emptyShape, out _, out _)); + var cargoData = RandomItem(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(2, item => - item.Behaviors.Any(b => b is CapacitorData) && - item.Shape.FitsWithin(emptyShape, out _, out _)); + var capacitorData = RandomItem(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); diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 1fbd7ef0d..3f1a444f9 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -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` 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` 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.