diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 5a38d5181..3bfe95ce4 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Assets/Scripts/UI/Menu/TradeMenu.cs b/Assets/Scripts/UI/Menu/TradeMenu.cs index 014525caf..8e042e269 100644 --- a/Assets/Scripts/UI/Menu/TradeMenu.cs +++ b/Assets/Scripts/UI/Menu/TradeMenu.cs @@ -259,33 +259,30 @@ public class TradeMenu : MonoBehaviour if (fieldType == typeof(float)) columns.Add((field.Name, 1, x => { - var behavior = x.LegacyEquippableData.Behaviors.FirstOrDefault(b => type.IsInstanceOfType(b)); - return () => ActionGameManager.RuntimePlayerSettings.Format((float) field.GetValue(behavior)); + var value = GetTypedBehaviorNumber(x, type, field); + return () => ActionGameManager.RuntimePlayerSettings.Format((float)value); }, x => { - var behavior = x.LegacyEquippableData.Behaviors.FirstOrDefault(b => type.IsInstanceOfType(b)); - return (float) field.GetValue(behavior); + return (float)GetTypedBehaviorNumber(x, type, field); })); else if (fieldType == typeof(int)) columns.Add((field.Name, 1, x => { - var behavior = x.LegacyEquippableData.Behaviors.FirstOrDefault(b => type.IsInstanceOfType(b)); - return () => ((int) field.GetValue(behavior)).ToString(); + var value = GetTypedBehaviorNumber(x, type, field); + return () => ((int)value).ToString(); }, x => { - var behavior = x.LegacyEquippableData.Behaviors.FirstOrDefault(b => type.IsInstanceOfType(b)); - return (int) field.GetValue(behavior); + return (int)GetTypedBehaviorNumber(x, type, field); })); else if (fieldType == typeof(PerformanceStat)) { columns.Add((field.Name, 1, x => { - var behavior = x.LegacyEquippableData.Behaviors.FirstOrDefault(b => type.IsInstanceOfType(b)); - return () => ActionGameManager.RuntimePlayerSettings.Format(((PerformanceStat) field.GetValue(behavior)).Max); + var value = GetTypedBehaviorNumber(x, type, field); + return () => ActionGameManager.RuntimePlayerSettings.Format((float)value); }, x => { - var behavior = x.LegacyEquippableData.Behaviors.FirstOrDefault(b => type.IsInstanceOfType(b)); - return ((PerformanceStat) field.GetValue(behavior)).Max; + return (float)GetTypedBehaviorNumber(x, type, field); })); } } @@ -404,10 +401,70 @@ public class TradeMenu : MonoBehaviour return ActionGameManager.RuntimeCatalog?.FindItemByLegacyId(item.Data.ItemId.ToString("D")); } + private static double GetTypedBehaviorNumber(TradeRow row, Type behaviorType, FieldInfo field) + { + var key = field.GetCustomAttribute()?.Key; + if (key == null) + { + return 0; + } + + var payload = FindTypedBehaviorPayload(row.TypedItem, behaviorType); + var payloadField = payload?.Fields.FirstOrDefault(candidate => candidate.Key == key.Value); + if (payloadField == null) + { + return 0; + } + + if (field.FieldType == typeof(PerformanceStat)) + { + return payloadField.Value.Children.Count > 1 + ? payloadField.Value.Children[1].NumberValue + : 0; + } + + return payloadField.Value.NumberValue; + } + + private static AetheriaRuntimeBehaviorPayload FindTypedBehaviorPayload(AetheriaRuntimeCatalogItem typedItem, Type behaviorType) + { + if (typedItem == null) + { + return null; + } + + return typedItem.BehaviorPayloads.FirstOrDefault(payload => TypedBehaviorMatches(payload, behaviorType)); + } + + private static bool TypedBehaviorMatches(AetheriaRuntimeBehaviorPayload payload, Type behaviorType) + { + if (string.Equals(payload.Kind, behaviorType.Name, StringComparison.Ordinal)) + { + return true; + } + + var payloadType = ResolveBehaviorType(payload.Kind); + return payloadType != null && behaviorType.IsAssignableFrom(payloadType); + } + + private static Type ResolveBehaviorType(string kind) + { + return string.IsNullOrWhiteSpace(kind) + ? null + : BehaviorTypesByName.TryGetValue(kind, out var type) + ? type + : null; + } + + private static readonly IReadOnlyDictionary BehaviorTypesByName = typeof(BehaviorData) + .GetAllChildClasses() + .Where(type => !string.IsNullOrWhiteSpace(type.Name)) + .GroupBy(type => type.Name) + .ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal); + private sealed class TradeRow { private readonly ItemManager _itemManager; - private ItemData _legacyData; public TradeRow(ItemInstance item, AetheriaRuntimeCatalogItem typedItem, ItemManager itemManager) { @@ -420,10 +477,6 @@ public class TradeMenu : MonoBehaviour public AetheriaRuntimeCatalogItem TypedItem { get; } - public ItemData LegacyData => _legacyData ?? (_legacyData = _itemManager.GetData(Item)); - - public EquippableItemData LegacyEquippableData => LegacyData as EquippableItemData; - public Guid LegacyId => Item?.Data?.ItemId ?? Guid.Empty; public string Name => !string.IsNullOrWhiteSpace(TypedItem?.Name) ? TypedItem.Name : "Unknown Item"; diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 4a3e5fc2f..465c5c027 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -155,12 +155,12 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. now comes from typed state, not `AetherDB.msgpack`. The surviving trade menu wraps rows in a typed `TradeRow`: name, mass, price, size, hardpoint type, commodity subtype, hull ownership, and behavior-kind filters read typed - catalog rows before any legacy projection is hydrated. Legacy `ItemData` - hydration is lazy and bounded to behavior reflection columns. Trade buy - decisions for crafted price, ship-hull classification, simple commodity base - price, and simple commodity stack size now read typed catalog rows; the - existing inventory transfer and ship-construction paths still own the actual - runtime mutation. + catalog rows before any legacy projection is hydrated. Dynamic behavior + columns read typed behavior payload fields by legacy payload key instead of + hydrating `BehaviorData` DTOs. Trade buy decisions for crafted price, + ship-hull classification, simple commodity base price, and simple commodity + stack size also read typed catalog rows; the existing inventory transfer and + ship-construction paths still own the actual runtime mutation. The sector properties UI also resolves station/turret/ship counts only through typed hull classifications from the runtime catalog; missing typed hull rows no longer fall back to legacy `HullData`. Loot pickup @@ -643,11 +643,13 @@ First Aetheria surfaces to publish: typed catalog row prefilters before row-level `ItemData` hydration. - Done: move trade menu row presentation for name, mass, price, size, hardpoint type, commodity subtype, hull-owned counts, and behavior-kind - filtering onto typed `TradeRow` fields; legacy `ItemData` hydration is - lazy and bounded to behavior reflection columns. + filtering onto typed `TradeRow` fields. - Done: move trade menu buy price, ship-hull classification, and simple commodity stack-size decisions onto typed catalog rows; inventory transfer and ship construction remain the runtime mutation owners. + - Done: move TradeMenu dynamic behavior columns onto typed behavior payload + fields; the menu no longer hydrates `ItemData` or `BehaviorData` for row + display, filtering, sorting, or buy decisions. - Done: move action-bar consumable drop binding onto typed catalog rows; legacy `ConsumableItemData` resolution is lazy and bounded to activation and active-duration fill until typed consumable effect execution exists.