Index cargo by typed item keys

This commit is contained in:
Meta Vens 2026-06-14 23:09:49 +01:00
commit df59a9da62
9 changed files with 120 additions and 51 deletions

Binary file not shown.

View file

@ -83,8 +83,10 @@ legacy catalog group/table annotations; their remaining metadata is projection
and inspection metadata for the typed runtime bridge.
Item instances now carry `AetheriaRuntimeItemReference`, a typed item-key
facade. `ItemInstance.ItemKey` is the identity surface, while `ItemId` is a
derived legacy-GUID compatibility projection for unfinished cargo and
simulation paths.
derived legacy-GUID compatibility projection for behavior config and broader
simulation paths. Cargo inventory indexes, consumable activation lookup,
action-bar consumable quantity/fill, transfer lookup, and trade owned counts
now use item keys.
The old generic `RuntimeCatalogLink<T>` abstraction, `ItemManager.GetData`
hydration path, vague `ItemInstance.Data` identity field, and old
`RuntimeItemReference.Projection` cache have been deleted; runtime item facts

View file

@ -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)
: "";
}
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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;
}
}
}

View file

@ -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<Guid, List<ItemInstance>> ItemsOfType = new Dictionary<Guid, List<ItemInstance>>();
public readonly Dictionary<string, List<ItemInstance>> ItemsOfType = new Dictionary<string, List<ItemInstance>>();
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<ItemInstance>();
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<ItemInstance>();
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<ItemInstance>();
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);

View file

@ -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<SimpleCommodity>().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<SimpleCommodity>().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<SimpleCommodity>().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<SimpleCommodity>().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;

View file

@ -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<T>` bridge
after loadout generation stopped hydrating selected typed rows into
`EquippableItemData` merely to instantiate equipment.