From 1806ba221ecd2006ba86bad2dcf4b7d78feccdce Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 13 Jun 2026 23:20:16 +0100 Subject: [PATCH] Prefilter loadouts with typed catalog rows --- .voidbot/state/aetheria.cc | Bin 53336 -> 53626 bytes Aetheria.State/README.md | 3 + .../Scripts/ServerShared/LoadoutGenerator.cs | 80 ++++++++++++++---- docs/aetheria-perfect-machine-map.md | 17 +++- 4 files changed, 80 insertions(+), 20 deletions(-) diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index d66b06bc62b2ffa3c129ed2fac95f4d466b436e3..fe8895e1de80f3a71b9be9b1796bfd4d691f4614 100644 GIT binary patch delta 1344 zcmaJ>O>9j;6xJWK5}QU_B4Vs0(mtxR{z8bT25D2zrW={wd+xoXb7!uZxvj_IMY_JcCH%=W7L$P~^n6EC@Yyyt?IKbwr5rkoYo zB&A1~%>ZXUn_`@q3bC|-x&(90@fqZNvOLxpvd5G*$;z8e;kQ$`h* zj+T(vzK8*f%~%sO31Xv0<^)p)9(4eJ{A)fDtyok|VuazvWRj21+N%N8=u4!-IjF8Y80;wjz(_hx~ z%-us1E}b36JJe6SvF*wC@!r~*`>Pv{jGm(r7I36Ou7wW&IM-^9{tZ}^_L%tzHA&{4 z{~A19PB-Weo2yt9J2ajx%!Cyo1j~PS?$M4?M(TXBJhnHfmV1-0^Xrqh$2V7X28by3 zpT^YE)r2dN)Q~TY1DvB<{@p0MHTiP=Z+CDBd{B|XRn(;gm|-pm2PUBQV{vO|SN;+W z3{2iP)JF|_Yg=D+wd{J4OVrW9tZfUudoW^~TQ522Qf0zNI5S?N3(P sdXsDK-xiPA&_MpYtxGP}Wf2|TlSY^8*Bg<2Hd4F!;az7DI&g6EH#0Z#NdN!< delta 1043 zcmY*YO-NKx6i!FPF`}@q#!#Y&LAa?q@12>s`R3l?-uq@6H)1VY z#oZQYSwV|5W>XYo3v*e!GJuAP89m!E&e}ujDOs43`43C0DWV#DzApG01Bf@ zz#Ip)jy-Wa#&es08eNX4*kv>qD~~S)Z_iC%fFjexDQM(^k@ox2p?DuK;X-oC7l!9B zC{X=0MGcAU|6d?!55MTNJu9uE53~eBB0XL}xF)zM!N_Eiq<|+ypfj&f6HOhyGNn8< zt+C&xvg!aat6j>XK&7)hAA4p{XtHASmHn}}_l7py^QyLxRUT%TQwpseC801nJ)t2j z72s{=7)Wzt0_y{PDa^70rz=O2mC6qd?t_$Xwvn?-BRxlfx(8H*%Uxy=(o{-^HNtsu zmlWAOYhA}bkNL(|2qCO6<#W%4qExe>v(l%t!o0^E3|yY4f}TgBpvX-YDqe&isXSKe z&m!ijQ3`, 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.