From df59a9da62d3ec364bb9e7c9b06dde4266b7dc39 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 14 Jun 2026 23:09:49 +0100 Subject: [PATCH] Index cargo by typed item keys --- .voidbot/state/aetheria.cc | Bin 89628 -> 89921 bytes Aetheria.State/README.md | 6 +- Assets/Scripts/Gameplay/ActionBarSlot.cs | 17 +-- .../ServerShared/Behaviors/ConstantWeapon.cs | 2 +- .../ServerShared/Behaviors/InstantWeapon.cs | 2 +- .../ServerShared/Behaviors/ItemUsage.cs | 4 +- Assets/Scripts/ServerShared/Entity.cs | 107 +++++++++++++----- Assets/Scripts/UI/Menu/TradeMenu.cs | 14 ++- docs/aetheria-perfect-machine-map.md | 19 +++- 9 files changed, 120 insertions(+), 51 deletions(-) diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index a2323796edbca39a97ad55666b52e26ac8fe872c..4481ec5dc81891194ab42bf45114eac1587e0f36 100644 GIT binary patch delta 1535 zcmaJ>-)kI29Ct2hOImFUO=xIE21TN<_oJq!Ng7&g660y><;V5q5X;=|-0jfana$4Z zCHs_0|AMf{BIrZ0p|8q4bY3EKiQ-l1HHFvjgrL;>S%%*6(dB$Zgj0d+7_)W_FpETL8^N*pX&*r6fo zP$zl(<7llzyok!uj_ncVKlo*FJz@bgn)#PVT<VS!>l{8fH?Df~n0I>;F|4Nw`f- z(K|FQ;rVoX9E~C?tsO@vx7H)V6;pA!#XJu?Oa>l_Eo>X?DkNOCXox)_u1A4|33qTk zBo4hod4$CVhl^|xaU~6@^@sw`tQYQol3wYKqFge;e?`w-qAuw$5#rOf;4RjcSSo!# zw88+7wuv3%Nzq)v}U{1CyLn%8T_o=BTee<4~FT= zrFbb~pl^qUo*+&M*Ifa_df(uPX6z9~F;TjHtj7jOIs~u*SLUUEzlmOW(T6thOuH9S zP>F)fFhS(J!G`36#j%y&7X*l-poVc33MaBN6vfP^OfAM8x(VHytF1t!Vv~erK;r;9 zEpySxj02+Fbmwg};c7|BeK`!ROv_vvyup+^Xu&>4OEcpTF^>Y#zA71#%<6qfx+_1O zJ<@pp3JHLLEkZ|Tl9d00(|`d|25=}{78uJy@P=`n`)1GerK{D2p44e=3SH}#A6bQ^ zw@kF2;A;yn^10OOGROsP$%2t#*0FRWogbaW^2gs<77Pz8tiUodo~2lcfyP`Kz3z zpBzHtqo%T`-Ph*Z!Vj4D;SDsA?j1rG({D%6>}daqoSJOhR-`Q-KGWDeei+>wKDuMt z@8s>fKOaRC^G62illwy_^Jn)=sZx2p_a5IJ_&J@w#Rt=u){wt{a3*`bKVL&z13xEq P_3la?eKj)hce?Q}JA@>O delta 1351 zcmZuxPiS04824>jNfSYAQ#a7dr&3FC^O}S;npG-oYSJYXwcUV5p_$CgzBlB(H!m~u zcHM*I;K`dZ$RdarBc2MvBJof~4>?Ft+CvMKKtZqEM@F4}P~<8kwrSJX5Pq`>$5*{h_kmE`9P;e`#iV`7`gRl|vs6!d}6!nbL(j zy^{y%ug`yRF-weyIS9jJ5Q#PW-0C>4JB-_VGN_z$nLpM6;IG$NEU zTfPSyBw?V9R0za`eD6xU4VqHLutGSlDnWb)`ZD89kq9GXQj2&%Xs7nNV5e7nsN-&Z zkCE?w7BK@tbtzLNVuI|{KK*@+I#bOJ1X6)^C?fuNk~)$f&}}Li=Lz@FL6$f)I&@MH z6-darh&YQ-6`24N)V+xkT>IY|o@o5L{m)z;8z`BjB3TY9uvCZjj5yseu<~dYlsMEV zk%%J5G>3&uumUQBRZfXpL#^?tjmoIWoRlGH@vc1WjiXvvY(%VmXrLI1H@V_icc7U8 z8j>WyaM@0{(xFH}ha}~&ihE4v-Y`&*EGVAR|3j|>u?SSo*{**-=KbAWKkgl~aIgG~ zRIo}EpIBI`pUaJ8FaL0K1LiXa(?LLHq0Ax%v25XjlLi&Yb(_GGkE!vXrjS=8FFgYz zb5XGF6;~uwpGVv{X*d*KDU?Ig9vQe6ufi74QuS_KuZ|vm>jFtpA}SR_?>P6{iJkG| zE{EbV!D=S~Q&d!M% zYljZUys|wJ@fMGA_~(cr5O!Fhrf=ZSZ>PK` abstraction, `ItemManager.GetData` hydration path, vague `ItemInstance.Data` identity field, and old `RuntimeItemReference.Projection` cache have been deleted; runtime item facts diff --git a/Assets/Scripts/Gameplay/ActionBarSlot.cs b/Assets/Scripts/Gameplay/ActionBarSlot.cs index fc3a0d1f8..e9b4e3d9e 100644 --- a/Assets/Scripts/Gameplay/ActionBarSlot.cs +++ b/Assets/Scripts/Gameplay/ActionBarSlot.cs @@ -82,20 +82,20 @@ public class ActionBarConsumableBinding : ActionBarBinding public override void Update() { - Slot.QuantityRemaining.text = $"{Entity.CountItemsInCargo(TargetLegacyId)}"; - if (TargetLegacyId == Guid.Empty) + Slot.QuantityRemaining.text = $"{Entity.CountItemsInCargo(TargetItemKey)}"; + if (string.IsNullOrWhiteSpace(TargetItemKey)) { Slot.Fill.fillAmount = 0; return; } - var instance = Entity.FindActiveConsumable(TargetLegacyId); + var instance = Entity.FindActiveConsumable(TargetItemKey); if (instance == null) Slot.Fill.fillAmount = 0; else Slot.Fill.fillAmount = instance.RemainingDuration / instance.Duration; } - private Guid TargetLegacyId => - Guid.TryParse(Target.LegacyId, out var legacyId) ? legacyId : Guid.Empty; + private string TargetItemKey => + Guid.TryParse(Target.LegacyId, out var legacyId) ? AetheriaRuntimeItemReference.FromLegacyId(legacyId) : ""; public string TargetItemDefinitionLegacyId => Target?.LegacyId ?? ""; @@ -116,8 +116,11 @@ public class ActionBarGearBinding : ActionBarBinding { get { - var itemId = Item?.EquippableItem?.ItemId ?? Guid.Empty; - return itemId == Guid.Empty ? "" : itemId.ToString("D"); + var itemKey = Item?.EquippableItem?.ItemKey ?? ""; + const string legacyPrefix = "aetheria.item_definition:legacy:"; + return itemKey.StartsWith(legacyPrefix, StringComparison.OrdinalIgnoreCase) + ? itemKey.Substring(legacyPrefix.Length) + : ""; } } diff --git a/Assets/Scripts/ServerShared/Behaviors/ConstantWeapon.cs b/Assets/Scripts/ServerShared/Behaviors/ConstantWeapon.cs index b1e739689..11409bc11 100644 --- a/Assets/Scripts/ServerShared/Behaviors/ConstantWeapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/ConstantWeapon.cs @@ -107,7 +107,7 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior var cargo = Entity.FindItemInCargo(_data.AmmoType); if (cargo != null) { - var item = cargo.ItemsOfType[_data.AmmoType][0]; + var item = cargo.GetFirstItem(_data.AmmoType); if (item is SimpleCommodity simpleCommodity) cargo.Remove(simpleCommodity, 1); diff --git a/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs b/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs index 96b67d27f..85921bae4 100644 --- a/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs @@ -132,7 +132,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior var cargo = Entity.FindItemInCargo(_data.AmmoType); if (cargo != null) { - var item = cargo.ItemsOfType[_data.AmmoType][0]; + var item = cargo.GetFirstItem(_data.AmmoType); if (item is SimpleCommodity simpleCommodity) cargo.Remove(simpleCommodity, 1); } diff --git a/Assets/Scripts/ServerShared/Behaviors/ItemUsage.cs b/Assets/Scripts/ServerShared/Behaviors/ItemUsage.cs index c65f368e4..949543508 100644 --- a/Assets/Scripts/ServerShared/Behaviors/ItemUsage.cs +++ b/Assets/Scripts/ServerShared/Behaviors/ItemUsage.cs @@ -39,7 +39,7 @@ public class ItemUsage : Behavior var cargo = Entity.FindItemInCargo(_data.Item); if (cargo == null) return false; - var item = cargo.ItemsOfType[_data.Item][0]; + var item = cargo.GetFirstItem(_data.Item); if (item is SimpleCommodity simpleCommodity) cargo.Remove(simpleCommodity, 1); if (item is CraftedItemInstance craftedItemInstance) @@ -47,4 +47,4 @@ public class ItemUsage : Behavior return true; } -} \ No newline at end of file +} diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 71bf00f45..a3b56c0a1 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -315,31 +315,43 @@ public abstract class Entity _activeConsumables.Add(new ConsumableItemEffect(item, this)); } + public ConsumableItemEffect FindActiveConsumable(string itemKey) + { + return _activeConsumables.FirstOrDefault(ac => ac.Item?.ItemKey == itemKey); + } + public ConsumableItemEffect FindActiveConsumable(Guid itemId) { - return _activeConsumables.FirstOrDefault(ac => ac.Item?.ItemId == itemId); + return FindActiveConsumable(AetheriaRuntimeItemReference.FromLegacyId(itemId)); } public bool CanActivateConsumable(AetheriaRuntimeCatalogItem item) { - var itemId = GetLegacyGuid(item); - return item != null && itemId != Guid.Empty && (item.Stackable || FindActiveConsumable(itemId) == null); + var itemKey = GetItemKey(item); + return item != null && !string.IsNullOrWhiteSpace(itemKey) && (item.Stackable || FindActiveConsumable(itemKey) == null); } public bool TryActivateConsumable(AetheriaRuntimeCatalogItem typedItem) { if (!CanActivateConsumable(typedItem)) return false; - var itemId = GetLegacyGuid(typedItem); - var bay = FindItemInCargo(itemId); + var itemKey = GetItemKey(typedItem); + var bay = FindItemInCargo(itemKey); if (bay == null) return false; - var item = (ConsumableItem)bay.ItemsOfType[itemId].First(); + var item = (ConsumableItem)bay.GetFirstItem(itemKey); ActivateConsumable(item); bay.Remove(item); return true; } + private static string GetItemKey(AetheriaRuntimeCatalogItem item) + { + return GetLegacyGuid(item) is var itemId && itemId != Guid.Empty + ? AetheriaRuntimeItemReference.FromLegacyId(itemId) + : ""; + } + private static Guid GetLegacyGuid(AetheriaRuntimeCatalogItem item) { return item != null && Guid.TryParse(item.LegacyId, out var legacyId) @@ -409,23 +421,33 @@ public abstract class Entity Temperature[position.x, position.y] += heat / ThermalMass[position.x, position.y]; } - public int CountItemsInCargo(Guid itemDataID) + public int CountItemsInCargo(string itemKey) { int sum = 0; foreach (var x in CargoBays) { - if (x.ItemsOfType.ContainsKey(itemDataID)) + if (x.ItemsOfType.ContainsKey(itemKey)) { - foreach (var i in x.ItemsOfType[itemDataID]) sum += i is SimpleCommodity simpleCommodity ? simpleCommodity.Quantity : 1; + foreach (var i in x.ItemsOfType[itemKey]) sum += i is SimpleCommodity simpleCommodity ? simpleCommodity.Quantity : 1; } } return sum; } + public int CountItemsInCargo(Guid itemDataID) + { + return CountItemsInCargo(AetheriaRuntimeItemReference.FromLegacyId(itemDataID)); + } + + public EquippedCargoBay FindItemInCargo(string itemKey) + { + return CargoBays.FirstOrDefault(c => c.ContainsItem(itemKey)); + } + public EquippedCargoBay FindItemInCargo(Guid itemDataID) { - return CargoBays.FirstOrDefault(c => c.ItemsOfType.ContainsKey(itemDataID)); + return FindItemInCargo(AetheriaRuntimeItemReference.FromLegacyId(itemDataID)); } public Shape UnoccupiedSpace @@ -450,14 +472,15 @@ public abstract class Entity // Returns the number of items successfully transferred public int TryTransferItems(Entity target, Guid itemDataID, int quantity) { + var itemKey = AetheriaRuntimeItemReference.FromLegacyId(itemDataID); int quantityTransferred = 0; while (quantityTransferred < quantity) { - EquippedCargoBay originInventory = CargoBays.FirstOrDefault(c => c.ItemsOfType.ContainsKey(itemDataID)); + EquippedCargoBay originInventory = CargoBays.FirstOrDefault(c => c.ContainsItem(itemKey)); if (originInventory == null) break; - var itemInstance = originInventory.ItemsOfType[itemDataID][0]; + var itemInstance = originInventory.GetFirstItem(itemKey); if (itemInstance is SimpleCommodity simpleCommodity) { @@ -1557,7 +1580,7 @@ public class EquippedCargoBay : EquippedItem public readonly ItemInstance[,] Occupancy; - public readonly Dictionary> ItemsOfType = new Dictionary>(); + public readonly Dictionary> ItemsOfType = new Dictionary>(); public Shape InteriorShape { get; } @@ -1576,6 +1599,21 @@ public class EquippedCargoBay : EquippedItem } } + public bool ContainsItem(string itemKey) + { + return !string.IsNullOrWhiteSpace(itemKey) && ItemsOfType.ContainsKey(itemKey); + } + + public ItemInstance GetFirstItem(string itemKey) + { + return ContainsItem(itemKey) ? ItemsOfType[itemKey].FirstOrDefault() : null; + } + + public ItemInstance GetFirstItem(Guid itemId) + { + return GetFirstItem(AetheriaRuntimeItemReference.FromLegacyId(itemId)); + } + public EquippedCargoBay(ItemManager itemManager, EquippableItem item, int2 position, Entity entity, string name) : base(itemManager, item, position, entity) { var typedCargoBay = ItemManager.GetRuntimeItem(EquippableItem); @@ -1627,7 +1665,7 @@ public class EquippedCargoBay : EquippedItem // For simple commodities, search for existing item stacks to add to foreach (var cargoItem in Cargo.Keys) { - if (item.ItemId != cargoItem.ItemId) continue; + if (item.ItemKey != cargoItem.ItemKey) continue; var cargoCommodity = (SimpleCommodity) cargoItem; if (cargoCommodity.Quantity >= maxStack) continue; @@ -1719,11 +1757,9 @@ public class EquippedCargoBay : EquippedItem } Cargo[item] = cargoCoord; - if(!ItemsOfType.ContainsKey(item.ItemId)) - ItemsOfType[item.ItemId] = new List(); - ItemsOfType[item.ItemId].Add(item); + AddTypedItemIndex(item); } - else if (Occupancy[cargoCoord.x, cargoCoord.y] is SimpleCommodity cargoCommodity && cargoCommodity.ItemId == item.ItemId) + else if (Occupancy[cargoCoord.x, cargoCoord.y] is SimpleCommodity cargoCommodity && cargoCommodity.ItemKey == item.ItemKey) { if (cargoCommodity.Quantity + item.Quantity <= maxStack) { @@ -1763,9 +1799,7 @@ public class EquippedCargoBay : EquippedItem } Cargo[item] = cargoCoord; - if(!ItemsOfType.ContainsKey(item.ItemId)) - ItemsOfType[item.ItemId] = new List(); - ItemsOfType[item.ItemId].Add(item); + AddTypedItemIndex(item); Mass += ItemManager.GetMass(item); ThermalMass += ItemManager.GetThermalMass(item); @@ -1787,9 +1821,7 @@ public class EquippedCargoBay : EquippedItem Occupancy[v.x, v.y] = null; Cargo.Remove(item); - ItemsOfType[item.ItemId].Remove(item); - if (!ItemsOfType[item.ItemId].Any()) - ItemsOfType.Remove(item.ItemId); + RemoveTypedItemIndex(item); Mass -= ItemManager.GetMass(item); ThermalMass -= ItemManager.GetThermalMass(item); @@ -1815,9 +1847,7 @@ public class EquippedCargoBay : EquippedItem Occupancy[v.x, v.y] = null; Cargo.Remove(item); - ItemsOfType[item.ItemId].Remove(item); - if (!ItemsOfType[item.ItemId].Any()) - ItemsOfType.Remove(item.ItemId); + RemoveTypedItemIndex(item); Mass -= ItemManager.GetMass(item); ThermalMass -= ItemManager.GetThermalMass(item); @@ -1837,6 +1867,29 @@ public class EquippedCargoBay : EquippedItem return ToShape(typedItem?.ShapeWidth ?? 1, typedItem?.ShapeHeight ?? 1, typedItem?.ShapeCells); } + private void AddTypedItemIndex(ItemInstance item) + { + var itemKey = item?.ItemKey ?? ""; + if (string.IsNullOrWhiteSpace(itemKey)) + return; + + if (!ItemsOfType.ContainsKey(itemKey)) + ItemsOfType[itemKey] = new List(); + + ItemsOfType[itemKey].Add(item); + } + + private void RemoveTypedItemIndex(ItemInstance item) + { + var itemKey = item?.ItemKey ?? ""; + if (string.IsNullOrWhiteSpace(itemKey) || !ItemsOfType.ContainsKey(itemKey)) + return; + + ItemsOfType[itemKey].Remove(item); + if (!ItemsOfType[itemKey].Any()) + ItemsOfType.Remove(itemKey); + } + private int GetMaxStack(SimpleCommodity item) { var typedItem = ItemManager.GetRuntimeItem(item); diff --git a/Assets/Scripts/UI/Menu/TradeMenu.cs b/Assets/Scripts/UI/Menu/TradeMenu.cs index bb36ffd74..ad11db3d4 100644 --- a/Assets/Scripts/UI/Menu/TradeMenu.cs +++ b/Assets/Scripts/UI/Menu/TradeMenu.cs @@ -293,18 +293,18 @@ public class TradeMenu : MonoBehaviour x => () => { if (x.IsHull) - return GameManager.DockedEntity.Children.Count(s => s.Hull.ItemId == x.LegacyId && s is Ship {IsPlayerShip: true}).ToString(); + return GameManager.DockedEntity.Children.Count(s => s.Hull.ItemKey == x.ItemKey && s is Ship {IsPlayerShip: true}).ToString(); if(x.Item is SimpleCommodity) - return (_targetCargo.ItemsOfType.ContainsKey(x.LegacyId) ? _targetCargo.ItemsOfType[x.LegacyId].Cast().Sum(s=>s.Quantity) : 0).ToString(); - return (_targetCargo.ItemsOfType.ContainsKey(x.LegacyId) ? _targetCargo.ItemsOfType[x.LegacyId].Count : 0).ToString(); + return (_targetCargo.ItemsOfType.ContainsKey(x.ItemKey) ? _targetCargo.ItemsOfType[x.ItemKey].Cast().Sum(s=>s.Quantity) : 0).ToString(); + return (_targetCargo.ItemsOfType.ContainsKey(x.ItemKey) ? _targetCargo.ItemsOfType[x.ItemKey].Count : 0).ToString(); }, x => { if (x.IsHull) - return GameManager.DockedEntity.Children.Count(s => s.Hull.ItemId == x.LegacyId && s is Ship {IsPlayerShip: true}); + return GameManager.DockedEntity.Children.Count(s => s.Hull.ItemKey == x.ItemKey && s is Ship {IsPlayerShip: true}); if(x.Item is SimpleCommodity) - return _targetCargo.ItemsOfType.ContainsKey(x.LegacyId) ? _targetCargo.ItemsOfType[x.LegacyId].Cast().Sum(s=>s.Quantity) : 0; - return _targetCargo.ItemsOfType.ContainsKey(x.LegacyId) ? _targetCargo.ItemsOfType[x.LegacyId].Count : 0; + return _targetCargo.ItemsOfType.ContainsKey(x.ItemKey) ? _targetCargo.ItemsOfType[x.ItemKey].Cast().Sum(s=>s.Quantity) : 0; + return _targetCargo.ItemsOfType.ContainsKey(x.ItemKey) ? _targetCargo.ItemsOfType[x.ItemKey].Count : 0; })); Spreadsheet.ShowData( @@ -471,6 +471,8 @@ public class TradeMenu : MonoBehaviour public Guid LegacyId => Item?.ItemId ?? Guid.Empty; + public string ItemKey => Item?.ItemKey ?? ""; + public string Name => !string.IsNullOrWhiteSpace(TypedItem?.Name) ? TypedItem.Name : "Unknown Item"; public float Mass => TypedItem != null ? (float)TypedItem.Mass : 0f; diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 7db5aea08..69280230a 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -53,7 +53,8 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. while hosting the CultMesh state node. `Aetheria.State.ApplyPending` remains a bounded local operator applicator for both pending lanes. - Shared item domain state is still partly built around `AetheriaRuntimeItemReference`, - `ItemData`, `RuntimeBehaviorConfig`, derived GUID compatibility, and runtime catalog metadata. Dead + `ItemData`, `RuntimeBehaviorConfig`, derived GUID compatibility in behavior + config and broader simulation paths, and runtime catalog metadata. Dead user-record and galaxy-map-layer catalog roots have been deleted, and surviving runtime DTOs no longer carry legacy catalog group/table annotations. Newtonsoft, JsonKnownTypes, RethinkDB, LiteNetLib client transport, and the broken @@ -620,7 +621,10 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. behavior metadata instead of rebuilding config DTOs for lookup. `ItemInstance.Reference` stores the typed item key, `ItemInstance.ItemKey` is the runtime identity surface, and `ItemInstance.ItemId` is a derived - legacy-GUID compatibility projection; + legacy-GUID compatibility projection. `EquippedCargoBay.ItemsOfType`, + consumable activation, action-bar consumable fill/quantity display, item + transfer lookup, and trade owned-count rows now use item keys rather than + GUID-owned cargo indexes; there is no remaining `ItemInstance.Data` identity/backing field. The reader interface for this bridge is named `IRuntimeItemCatalogReader` and exposes only typed catalog row lookup. Behavior @@ -1199,9 +1203,9 @@ First Aetheria surfaces to publish: `Aetheria.Shared.Unity`; the remaining MessagePack reference is contained in the typed state package's `.cc` reader. - In progress: convert domain references from GUID/base-class patterns to - typed record refs. Item instances now carry typed item keys; remaining - cargo/simulation dictionaries still derive legacy GUIDs until those - structures move to typed keys. + typed record refs. Item instances and cargo inventory indexes now carry + typed item keys; remaining behavior config and broader simulation paths + still derive legacy GUIDs until those structures move to typed keys. - Remove runtime dependency on `ItemData` DTOs as item instance owners. 5. Mesh host @@ -1318,6 +1322,11 @@ First Aetheria surfaces to publish: `AetheriaRuntimeItemReference` and make the reference typed-key-first. `ItemInstance.ItemKey` is now the primary identity surface; `ItemId` is derived legacy compatibility for unfinished cargo and simulation paths. + - Done: move `EquippedCargoBay.ItemsOfType`, consumable activation lookup, + action-bar consumable quantity/fill, item transfer lookup, and trade owned + counts from derived `Guid ItemId` keys to typed `ItemKey` strings. GUID + behavior-config fields still translate at the cargo boundary until + behavior config construction moves to typed item references. - Done: delete the dead `ItemManager.GetRuntimeItemProjection` bridge after loadout generation stopped hydrating selected typed rows into `EquippableItemData` merely to instantiate equipment.