From ce8adfbaf25d94460108f5be18c7ea5a276e5cfc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 14 Jun 2026 05:25:56 +0100 Subject: [PATCH] Use typed rows for loadout selection --- .voidbot/state/aetheria.cc | Bin 66565 -> 67673 bytes Assets/Scripts/ServerShared/ItemManager.cs | 5 + .../Scripts/ServerShared/LoadoutGenerator.cs | 188 +++++++++++------- docs/aetheria-perfect-machine-map.md | 25 ++- 4 files changed, 135 insertions(+), 83 deletions(-) diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 56c66ee9f1b7ecf2e0e692bb5aaa7bf8ded603a9..cc17f57a8dfb4a67b1730bc7558fd47e634e3794 100644 GIT binary patch delta 1703 zcmb7FO>7%Q6efuaNr9vZq@fZ;PeQ0Ijh*->aU2k+q<|2hk|rD)D#7f|*gIr*W;-)$ z$6O+WkXm}7jMM|C3QCa>NC98Cl?qiH0dXM}5;tz3atnw9Z+2~`v?ngMcIM5z@B7~O z-n@G@xBY%@>&oEXQf09?TUe+S*Def9+UtYoM|vys_U6vRHW@Pgm|7Y$oI5zU^GN4V zZh22<`RKWCZ8UQ3^wvv~P??zLnP7?-sX)jU#7xztiZrwsZv=)>@QI+D62m~Vkoks7 z0k{Nsw9bv;q5=31v0{|_7^XoA3wx*R)>@7}f$Q?k?y2LeCm?83Oy=(3E--8ZsT-z% zfN95=O(BLB7rO9?9}*tj7+5-q$z-%d3<+i91;Zl!e%BgtlWJ!3flqjKBoU zrQUS-?x2@w#MaNEX9A*VEV(cmkTUc?9i3^=CWA?(zwS7Cq@8W}L^b4$Ph#S8r&<=B zO2cTLBS6?x43kYmkc45HaT+`pkS3QZkC&mQ0rS`ObgE+-o@z(vHK*X4l3Y^FyUkeoKk z(ZjOGVWQzyQC}t-EBq~H&)oaq)89RpU`ZK-nKhv$)jH@vCLx7&#^T#7=zK>K6G-J` z<9*w0PCLE={KTZhcM1Imt>fB55|NF(Mi}5s+UKhcBk1%xN`gL75^LmSaWT7oFMLLX zj~Gs}y8h)Zc;{o)IeOpM19t4j{il|nk+cmFiSY(sDw-9dN3YRuz0{U@XMTs-0n>?U zq8)B8LIJO5_3g*)z{RP~#nE8=o@`;pl6J*jxjZ^}YSmtPZ_;&NN~Evl|L)_$=d8&o z6F$qBjoS-XU$fs_8||EhH-_!^hwgshm}-DPkDzhUUYKDKQXId*_(c7L>u%>ewh`k5 zVATHl&;fgMf3BLDt?M)NQzVh5W<+6M(4Ogbr%g-@jtHSY_#)gSuMn*_LI_6<&a*|k)@Zz+4}r~Tt!WeuFCM&{92vf26hkFk;7 zdr>XhABGMLXLr_$_RpaydwAz+ce_@c!{_9D_bI6r7t*KX!@u6{9Vod2oyEVejr3wl Hb8G(swzqIC delta 887 zcmZ8gJ!lj`6gHQ{{P=@nx#%47nDvd;)n0B^Nmmpq9cDMUYk! zUSvv-tIRWvT-kT#ZmqNo5RM)=k`y}}DFP@k4b(+ysg-7)7u$h~@TYeCfh5_NUd=RwD6wuIO+$)2FzR=jFER1W+Xl9!NqTTz`^))TylgC3HO0>a#TbHX57|*5qqxeKNV3A)1Bqh{ORM2%Q6ka-on*T<))6cS%SE&% zbd>QTsNMfr0vUR!GS96cSIARXQ1k0{MMq{|*O)&Io7-+2mDIJtV71&TD+WH`7wPl0@Iz_uDf~(L+38GNZJHogV7sZYxbF7$a zk0ci6_L=FHbX5B6Zrwa({W{1g#`+$Zmxs?Zyc!@%p<}-8+-7Dc#-j7z7aQ))q|MX< zq;rGWTz{^&Kii$@J hullFilter = null) + public RuntimeEntityBlueprint GenerateShipLoadout(Predicate hullFilter = null) { var hullData = RandomHull(HullType.Ship, hullFilter); if(hullData==null) @@ -93,12 +93,11 @@ public class LoadoutGenerator var emptyShape = entity.UnoccupiedSpace; - var dockingBayData = RandomItem(2, - item => item.Shape.FitsWithin(emptyShape, out _, out _), - item => FitsWithin(item, emptyShape)); + var dockingBayRow = RandomCatalogItem(2, item => FitsWithin(item, emptyShape)); + var dockingBayData = ProjectRuntimeItem(dockingBayRow); if (dockingBayData == null) throw new InvalidLoadoutException("No compatible docking bay found for station!"); - dockingBayData.Shape.FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition); + ToShape(dockingBayRow).FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition); var dockingBay = ItemManager.CreateInstance(dockingBayData) as EquippableItem; dockingBay.Rotation = cargoRotation; if (!entity.TryEquip(dockingBay, cargoPosition)) @@ -109,10 +108,12 @@ 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), - item => item.Category != "CargoBayData" && item.Category != "DockingBayData" && + IEnumerable inventory = RandomCatalogItems(16, 1, + item => item.Category != "CargoBayData" && item.Category != "DockingBayData" && (item.Category != "HullData" || item.HullType == nameof(HullType.Ship))); + inventory = inventory + .Select(ProjectRuntimeItem) + .Where(item => item != null); inventory = inventory .OrderByDescending(item=>item.Shape.Coordinates.Length); foreach (var item in inventory) @@ -126,18 +127,17 @@ public class LoadoutGenerator return RuntimeEntityBlueprintProjector.CaptureBlueprint(entity) as RuntimeOrbitalEntityBlueprint; } - public HullData RandomHull(HullType type, Predicate hullFilter = null) + public HullData RandomHull(HullType type, Predicate hullFilter = null) { - return RandomItem(0, item => - (hullFilter?.Invoke(item) ?? true) && - item.HullType == type, - item => string.Equals(item.HullType, type.ToString(), StringComparison.Ordinal)); + var hullRow = RandomCatalogItem(0, item => + string.Equals(item.HullType, type.ToString(), StringComparison.Ordinal) && + (hullFilter?.Invoke(item) ?? true)); + return ProjectRuntimeItem(hullRow); } - public T[] RandomItems( + public AetheriaRuntimeCatalogItem[] RandomCatalogItems( int count, float sizeExponent, - Predicate filter = null, Predicate typedFilter = null) where T : EquippableItemData { return RuntimeCatalog.EquipmentItems @@ -149,20 +149,24 @@ public class LoadoutGenerator (Galaxy.IsPrelude || Galaxy.ContainsFaction(manufacturer) && (Faction == null || Faction.Allegiance.ContainsKey(manufacturer))) && (typedFilter?.Invoke(item) ?? true)) - .Select(ProjectRuntimeItem) - .Where(item => item != null) - .Where(item => - item.Price > 0 && - item.Manufacturer != Guid.Empty && - (Galaxy.IsPrelude || Galaxy.ContainsFaction(item.Manufacturer) && - (Faction == null || Faction.Allegiance.ContainsKey(item.Manufacturer))) && - (filter?.Invoke(item) ?? true)) .WeightedRandomElements(ref Random, item => - Faction == null ? 1 : - (item.Manufacturer == Faction.ID ? 1 : Faction.Allegiance.ContainsKey(item.Manufacturer) ? Faction.Allegiance[item.Manufacturer] : 0.0f / // Prioritize items from allied manufacturers - ManufacturerDistancePenalty(item.Manufacturer)) * // Penalize distance to manufacturer headquarters - pow(item.Shape.Coordinates.Length, sizeExponent) / // Prioritize larger items - pow(item.Price, PriceExponent), // Penalize item price to a controllable degree + { + if (!Guid.TryParse(item.ManufacturerLegacyId, out var manufacturer)) + { + return 0; + } + + var allegianceWeight = Faction == null + ? 1 + : manufacturer == Faction.ID + ? 1 + : Faction.Allegiance.TryGetValue(manufacturer, out var allegiance) + ? allegiance / ManufacturerDistancePenalty(manufacturer) + : 0; + return allegianceWeight * + pow(item.OccupiedCells, sizeExponent) / // Prioritize larger items + pow(item.Price, PriceExponent); // Penalize item price to a controllable degree + }, count ); } @@ -190,6 +194,11 @@ public class LoadoutGenerator private T ProjectRuntimeItem(AetheriaRuntimeCatalogItem item) where T : EquippableItemData { + if (item == null) + { + return null; + } + return Guid.TryParse(item.LegacyId, out var legacyId) ? ItemManager.GetRuntimeItemProjection(legacyId) : null; @@ -208,30 +217,27 @@ public class LoadoutGenerator : 1; } - public T RandomItem( + public AetheriaRuntimeCatalogItem RandomCatalogItem( float sizeExponent, - Predicate filter = null, Predicate typedFilter = null) where T : EquippableItemData { - return RandomItems(1, sizeExponent, filter, typedFilter).FirstOrDefault(); + return RandomCatalogItems(1, sizeExponent, typedFilter).FirstOrDefault(); } - public T RandomItem(HardpointData hardpoint, float sizeExponent, Predicate filter = null) where T : EquippableItemData + public AetheriaRuntimeCatalogItem RandomCatalogItem(AetheriaRuntimeHardpoint hardpoint, float sizeExponent, Predicate filter = null) where T : EquippableItemData { - return RandomItem( + return RandomCatalogItem( 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)); + item => FitsHardpoint(item, hardpoint) && (filter?.Invoke(item) ?? true)); } - private static bool FitsHardpoint(AetheriaRuntimeCatalogItem item, HardpointData hardpoint) + private static bool FitsHardpoint(AetheriaRuntimeCatalogItem item, AetheriaRuntimeHardpoint hardpoint) { - return string.Equals(item.HardpointType, hardpoint.Type.ToString(), StringComparison.Ordinal) && - item.OccupiedCells == hardpoint.Shape.Coordinates.Length && - FitsWithin(item, hardpoint.Shape, hardpoint.Rotation); + return item != null && + hardpoint != null && + string.Equals(item.HardpointType, hardpoint.Type, StringComparison.Ordinal) && + item.OccupiedCells == hardpoint.OccupiedCells && + FitsWithin(item, ToShape(hardpoint), GetRotation(hardpoint)); } private static bool HasBehaviorKind(AetheriaRuntimeCatalogItem item, string behaviorKind) @@ -242,12 +248,42 @@ public class LoadoutGenerator private static bool FitsWithin(AetheriaRuntimeCatalogItem item, Shape target) { - return ToShape(item.ShapeWidth, item.ShapeHeight, item.ShapeCells).FitsWithin(target, out _, out _); + if (item == null) + { + return false; + } + + return ToShape(item).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 _); + if (item == null) + { + return false; + } + + return ToShape(item).FitsWithin(target, rotation, out _); + } + + private static Shape ToShape(AetheriaRuntimeCatalogItem item) + { + if (item == null) + { + throw new InvalidLoadoutException("Typed catalog shape was requested before a compatible item was selected."); + } + + return ToShape(item.ShapeWidth, item.ShapeHeight, item.ShapeCells); + } + + private static Shape ToShape(AetheriaRuntimeHardpoint hardpoint) + { + if (hardpoint == null) + { + throw new InvalidLoadoutException("Typed hardpoint shape was requested before a hardpoint was selected."); + } + + return ToShape(hardpoint.ShapeWidth, hardpoint.ShapeHeight, hardpoint.ShapeCells); } private static Shape ToShape(int width, int height, IReadOnlyList cells) @@ -261,64 +297,77 @@ public class LoadoutGenerator return shape; } + private static ItemRotation GetRotation(AetheriaRuntimeHardpoint hardpoint) + { + return Enum.TryParse(hardpoint.Rotation, out ItemRotation rotation) + ? rotation + : ItemRotation.None; + } + private void OutfitEntity(Entity entity) { - var hullData = ItemManager.GetData(entity.Hull) as HullData; - foreach (var v in hullData.Shape.Coordinates) entity.HullConductivity[v.x, v.y] = true; - var previousItems = new List(); - foreach (var hardpoint in hullData.Hardpoints.OrderByDescending(h=>h.Shape.Coordinates.Length)) + var typedHull = ItemManager.GetRuntimeItem(entity.Hull); + if (typedHull == null) { - if (hardpoint.Type == HardpointType.ControlModule) + throw new InvalidLoadoutException("Selected hull is missing from the typed runtime catalog."); + } + + foreach (var v in ToShape(typedHull).Coordinates) entity.HullConductivity[v.x, v.y] = true; + var previousItems = new List(); + foreach (var hardpoint in typedHull.Hardpoints.OrderByDescending(h=>h.OccupiedCells)) + { + if (hardpoint.Type == nameof(HardpointType.ControlModule)) { var controllerBehaviorKind = entity is Ship ? nameof(CockpitData) : entity is OrbitalEntity ? nameof(TurretControllerData) : null; - var controllerData = RandomItem(hardpoint, 2, - item => item.Behaviors.Any(b => entity is Ship && b is CockpitData || entity is OrbitalEntity && b is TurretControllerData), - item => HasBehaviorKind(item, controllerBehaviorKind)); + var controllerRow = RandomCatalogItem(hardpoint, 2, item => HasBehaviorKind(item, controllerBehaviorKind)); + var controllerData = ProjectRuntimeItem(controllerRow); if (controllerData == null) throw new InvalidLoadoutException("No compatible controller found for entity!"); var controller = ItemManager.CreateInstance(controllerData) as EquippableItem; if (!entity.TryEquip(controller)) { - throw new InvalidLoadoutException($"Failed to equip selected {Enum.GetName(typeof(HardpointType), hardpoint.Type)}!"); + throw new InvalidLoadoutException($"Failed to equip selected {hardpoint.Type}!"); } } else { // If a previously selected item fits, use that one (this is why we must process larger hardpoints first) - var itemData = previousItems - .FirstOrDefault(i => i.HardpointType == hardpoint.Type && i.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _)); - var previousItem = entity.Equipment.FirstOrDefault(item => item.Data == itemData); - itemData ??= RandomItem(hardpoint, 2); - if (itemData == null) ItemManager.Log($"No compatible item found for entity {Enum.GetName(typeof(HardpointType), hardpoint.Type)} hardpoint!"); + var itemRow = previousItems.FirstOrDefault(i => FitsHardpoint(i, hardpoint)); + var previousItem = itemRow == null + ? null + : Guid.TryParse(itemRow.LegacyId, out var previousItemId) + ? entity.Equipment.FirstOrDefault(item => item.EquippableItem.Data.ItemId == previousItemId) + : null; + itemRow ??= RandomCatalogItem(hardpoint, 2); + var itemData = ProjectRuntimeItem(itemRow); + if (itemData == null) ItemManager.Log($"No compatible item found for entity {hardpoint.Type} hardpoint!"); else { - //throw new InvalidLoadoutException($"No compatible item found for entity {Enum.GetName(typeof(HardpointType), hardpoint.Type)} hardpoint!"); + //throw new InvalidLoadoutException($"No compatible item found for entity {hardpoint.Type} hardpoint!"); EquippableItem item; if(previousItem!=null) item = ItemManager.CreateInstance(itemData, previousItem.EquippableItem.Quality) as EquippableItem; else item = ItemManager.CreateInstance(itemData) as EquippableItem; if (!entity.TryEquip(item)) { - throw new InvalidLoadoutException($"Failed to equip selected {Enum.GetName(typeof(HardpointType), hardpoint.Type)}!"); + throw new InvalidLoadoutException($"Failed to equip selected {hardpoint.Type}!"); } - previousItems.Add(itemData); + previousItems.Add(itemRow); } } } var emptyShape = entity.UnoccupiedSpace; - var cargoData = RandomItem(3, - item => !(item is DockingBayData) && - item.Shape.FitsWithin(emptyShape, out _, out _), - item => item.Category != "DockingBayData" && FitsWithin(item, emptyShape)); + var cargoRow = RandomCatalogItem(3, item => item.Category != "DockingBayData" && FitsWithin(item, emptyShape)); + var cargoData = ProjectRuntimeItem(cargoRow); if (cargoData == null) throw new InvalidLoadoutException("No compatible cargo bay found for entity!"); - cargoData.Shape.FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition); + ToShape(cargoRow).FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition); var cargo = ItemManager.CreateInstance(cargoData) as EquippableItem; cargo.Rotation = cargoRotation; if (!entity.TryEquip(cargo, cargoPosition)) @@ -326,14 +375,13 @@ 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 capacitorRow = RandomCatalogItem(2, item => item.BehaviorKinds.Contains(nameof(CapacitorData), StringComparer.Ordinal) && FitsWithin(item, emptyShape)); + var capacitorData = ProjectRuntimeItem(capacitorRow); if (capacitorData == null) throw new InvalidLoadoutException("No compatible capacitor found for entity!"); - capacitorData.Shape.FitsWithin(emptyShape, out var capacitorRotation, out var capacitorPosition); + ToShape(capacitorRow).FitsWithin(emptyShape, out var capacitorRotation, out var capacitorPosition); var capacitor = ItemManager.CreateInstance(capacitorData) as EquippableItem; capacitor.Rotation = capacitorRotation; if (!entity.TryEquip(capacitor, capacitorPosition)) diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index c3882a6d1..e4b8c4c68 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -185,10 +185,13 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. and mass/thermal mass from runtime catalog rows. `Entity.MapEntity`, `Entity.UnoccupiedSpace`, grid-offset placement, and `Entity.UpdateTemperature` also read typed hull shape/interior, hardpoints, base armor, mass, specific - heat, and conductivity from runtime catalog rows. `EquippedItem` inset-shape - and behavior construction, docking bay max-size, and loadout generator helper - predicates still belong to legacy simulation projections until those owners - are rebuilt. Ship drag, combat/turret shot prediction height, and thruster + heat, and conductivity from runtime catalog rows. `LoadoutGenerator` + candidate weighting, hardpoint/cargo/capacitor fit, selected-item reuse, and + hull conductivity setup now use typed runtime catalog rows; selected DTO + hydration is only the remaining item-instantiation bridge. `EquippedItem` + inset-shape and behavior construction plus docking bay max-size still belong + to legacy simulation projections until those owners are rebuilt. Ship drag, + combat/turret shot prediction height, and thruster torque geometry now consume typed hull facets and typed shape masks. The item properties panel also reads typed catalog title names, descriptions, manufacturer, base mass, max durability, thermal bounds, and thermal @@ -648,15 +651,11 @@ First Aetheria surfaces to publish: - Done: move entity faction restore and loadout manufacturer-distance weighting to `Galaxy.ResolveFaction`, so legacy `Faction` catalog entries no longer decide runtime faction references after generation. - - 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: move loadout hull type and control-module behavior-kind checks onto - typed catalog rows before hydrating `HullData`/`BehaviorData` projections. + - 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. - 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