using System; using System.Collections; using System.Collections.Generic; using System.Linq; using GameCult.Aetheria.State.Unity; using GameCult.Eve.Surface; using GameCult.Eve.UnityUIToolkit; using TMPro; using UnityEngine; using UnityEngine.UI; using UnityEngine.UIElements; using Unity.Mathematics; using static Unity.Mathematics.math; public class TradeMenu : MonoBehaviour { private const string CargoSelectorSurfaceType = "surface-state"; private const string CargoSelectorSurfaceSchema = "gamecult.eve.surface.v1"; private const string CargoSelectorSurfaceProviderId = "aetheria"; private const string CargoSelectorSurfaceProviderKind = "trade.menu"; private const string CargoSelectorSurfaceId = "aetheria.trade.target_cargo_selector"; private const string CloseCargoSelectorCommand = "aetheria.trade.target_cargo_selector.close"; private const string FilterSurfaceId = "aetheria.trade.filter_selector"; private const string CloseFilterSurfaceCommand = "aetheria.trade.filter_selector.close"; private const string RowActionSurfaceId = "aetheria.trade.row_actions"; private const string CloseRowActionSurfaceCommand = "aetheria.trade.row_actions.close"; private const string TradeItemSurfaceId = "aetheria.trade.item_details"; private const string CloseTradeItemDetailsCommand = "aetheria.trade.item_details.close"; public ActionGameManager GameManager; public ConfirmationDialog Dialog; public UnityEngine.UI.Button NewFilterButton; public Prototype FilterPrototype; public SizeFilter MinimumSizeFilter; public SizeFilter MaximumSizeFilter; public Spreadsheet Spreadsheet; public TextMeshProUGUI TargetCargoLabel; public UnityEngine.UI.Button FoldoutButton; public TextMeshProUGUI CreditsLabel; private EquippedCargoBay _targetCargo; private (ItemFilter filter, HardpointType type) _hardpointFilter; private (ItemFilter filter, SimpleCommodityCategory type) _commodityFilter; private (ItemFilter filter, CompoundCommodityCategory type) _compoundCommodityFilter; private List _behaviorFilters = new List(); private readonly Dictionary _filterSurfaceCommands = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _rowActionSurfaceCommands = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _cargoSelectionCommands = new Dictionary(StringComparer.Ordinal); private UIDocument _cargoSelectorSurfaceDocument; private UIDocument _filterSurfaceDocument; private UIDocument _rowActionSurfaceDocument; private UIDocument _tradeItemSurfaceDocument; private string _rowActionTitle = "Trade Actions"; public EquippedCargoBay Inventory { get; set; } private void OnEnable() { if (GameManager.DockedEntity == null) return; _targetCargo = GameManager.DockingBay; TargetCargoLabel.text = "Docking Bay"; HideCargoSelectorSurface(); HideFilterSurface(); HideRowActionSurface(); HideTradeItemDetailsSurface(); UpdateCreditsLabel(); MinimumSizeFilter.Width.onEndEdit.RemoveAllListeners(); MinimumSizeFilter.Width.onEndEdit.AddListener(_ => Populate()); MinimumSizeFilter.Height.onEndEdit.RemoveAllListeners(); MinimumSizeFilter.Height.onEndEdit.AddListener(_ => Populate()); MaximumSizeFilter.Width.onEndEdit.RemoveAllListeners(); MaximumSizeFilter.Width.onEndEdit.AddListener(_ => Populate()); MaximumSizeFilter.Height.onEndEdit.RemoveAllListeners(); MaximumSizeFilter.Height.onEndEdit.AddListener(_ => Populate()); NewFilterButton.onClick.RemoveAllListeners(); NewFilterButton.onClick.AddListener(() => { RenderFilterSurface(); }); Populate(); } void Populate() { var columns = new List<(string name, int size, Func> output, Func sortKey)>(); columns.Add(("Name", 3, x => () => x.Item is CraftedItemInstance craftedItemInstance ? $"{x.Name}" : x.Name, x => x.Name)); if(_hardpointFilter.filter==null) columns.Add(("Type", 2, x => () => { if (x.TryGetTypedSimpleCommodityCategory(out _)) return x.TypedItem.SimpleCommodityCategory; if (x.TryGetTypedCompoundCommodityCategory(out _)) return x.TypedItem.CompoundCommodityCategory; if (x.TryGetTypedHardpoint(out var hardpointType)) return Enum.GetName(typeof(HardpointType), hardpointType); return "None"; }, x => { if (x.TryGetTypedSimpleCommodityCategory(out var simpleCategory)) return (int)simpleCategory; var offset = Enum.GetValues(typeof(SimpleCommodityCategory)).Length; if(x.TryGetTypedCompoundCommodityCategory(out var compoundCategory)) return (int)compoundCategory + offset; offset += Enum.GetValues(typeof(CompoundCommodityCategory)).Length; if (x.TryGetTypedHardpoint(out var hardpointType)) return (int) hardpointType + offset; return 0; })); columns.Add(("Mass", 1, x => () => ActionGameManager.RuntimePlayerSettings.Format(x.Mass), x => x.Mass)); columns.Add(("Price", 1, x => () => x.Price.ToString("N0"), x => x.Price)); columns.Add(("Size", 1, x => () => $"{x.ShapeWidth}x{x.ShapeHeight}", x => x.ShapeWidth * x.ShapeHeight)); var items = Inventory.Cargo.Keys .Where(PassesTypedTradeFilters) .Select(item => new TradeRow(item, FindTypedTradeItem(item), GameManager.ItemManager)); if (MinimumSizeFilter.gameObject.activeSelf) items = items.Where(i => !(MinimumSizeFilter.Width.text.Length > 0 && i.ShapeWidth < int.Parse(MinimumSizeFilter.Width.text) || MinimumSizeFilter.Height.text.Length > 0 && i.ShapeHeight < int.Parse(MinimumSizeFilter.Height.text))); if (MaximumSizeFilter.gameObject.activeSelf) items = items.Where(i => !(MaximumSizeFilter.Width.text.Length > 0 && i.ShapeWidth > int.Parse(MaximumSizeFilter.Width.text) || MaximumSizeFilter.Height.text.Length > 0 && i.ShapeHeight > int.Parse(MaximumSizeFilter.Height.text))); if(_commodityFilter.filter != null) items = items.Where(i => i.TryGetTypedSimpleCommodityCategory(out var category) && category == _commodityFilter.type); if(_compoundCommodityFilter.filter != null) items = items.Where(i => i.TryGetTypedCompoundCommodityCategory(out var category) && category == _compoundCommodityFilter.type); if (_hardpointFilter.filter != null) items = items.Where(i => i.TryGetTypedHardpoint(out var hardpointType) && hardpointType == _hardpointFilter.type); foreach (var behaviorFilter in _behaviorFilters) { items = items.Where(i => HasTypedBehavior(i.TypedItem, behaviorFilter)); foreach (var field in behaviorFilter.Metadata.DisplayFields) { if (field.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.Number) columns.Add((field.Name, 1, x => { var value = GetTypedBehaviorNumber(x, behaviorFilter, field); return () => ActionGameManager.RuntimePlayerSettings.Format((float)value); }, x => { return (float)GetTypedBehaviorNumber(x, behaviorFilter, field); })); else if (field.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.Temperature) columns.Add((field.Name, 1, x => { var value = GetTypedBehaviorNumber(x, behaviorFilter, field); return () => ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)value); }, x => { return (float)GetTypedBehaviorNumber(x, behaviorFilter, field); })); else if (field.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.Integer) columns.Add((field.Name, 1, x => { var value = GetTypedBehaviorNumber(x, behaviorFilter, field); return () => ((int)value).ToString(); }, x => { return (int)GetTypedBehaviorNumber(x, behaviorFilter, field); })); else if (field.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat) { columns.Add((field.Name, 1, x => { var value = GetTypedBehaviorNumber(x, behaviorFilter, field); return () => ActionGameManager.RuntimePlayerSettings.Format((float)value); }, x => { return (float)GetTypedBehaviorNumber(x, behaviorFilter, field); })); } } } columns.Add(("Owned", 1, x => () => { if (x.IsHull) 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.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.ItemKey == x.ItemKey && s is Ship {IsPlayerShip: true}); if(x.Item is SimpleCommodity) 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( columns.Select(x => x.name).ToArray(), columns.Select(x => x.size).ToArray(), items.Select(i => new SpreadsheetEntryRow { Columns = columns.Select(x => new SpreadsheetEntryColumn { Output = x.output(i), SortKey = x.sortKey(i) }).ToArray(), OnClick = () => RenderTradeItemDetailsSurface(i.TypedItem), OnDoubleClick = () => { switch (i.Item) { case CraftedItemInstance c: Buy(c); break; case SimpleCommodity s: Buy(s, 1); break; } Populate(); }, OnRightClick = () => { if (i.Item is SimpleCommodity s) { RenderRowActionSurface( $"Buying {i.Name}", ("Buy Quantity", () => ShowBuyQuantityDialog(i.Name, i.Price, s))); } } })); } private bool PassesTypedTradeFilters(ItemInstance item) { var typedItem = FindTypedTradeItem(item); if (typedItem == null) { return true; } if (MinimumSizeFilter.gameObject.activeSelf && (MinimumSizeFilter.Width.text.Length > 0 && typedItem.ShapeWidth < int.Parse(MinimumSizeFilter.Width.text) || MinimumSizeFilter.Height.text.Length > 0 && typedItem.ShapeHeight < int.Parse(MinimumSizeFilter.Height.text))) { return false; } if (MaximumSizeFilter.gameObject.activeSelf && (MaximumSizeFilter.Width.text.Length > 0 && typedItem.ShapeWidth > int.Parse(MaximumSizeFilter.Width.text) || MaximumSizeFilter.Height.text.Length > 0 && typedItem.ShapeHeight > int.Parse(MaximumSizeFilter.Height.text))) { return false; } if (_hardpointFilter.filter != null && !string.Equals(typedItem.HardpointType, _hardpointFilter.type.ToString(), StringComparison.Ordinal)) { return false; } return _behaviorFilters.All(filter => HasTypedBehavior(typedItem, filter)); } private static AetheriaRuntimeCatalogItem FindTypedTradeItem(ItemInstance item) { return ActionGameManager.RuntimeCatalog?.FindItem(item?.ItemKey ?? ""); } private static double GetTypedBehaviorNumber(TradeRow row, BehaviorFilter behaviorFilter, AetheriaRuntimeBehaviorFieldMetadata field) { var payload = FindTypedBehaviorPayload(row.TypedItem, behaviorFilter); var payloadField = payload?.Fields.FirstOrDefault(candidate => candidate.Key == field.Key); if (payloadField == null) { return 0; } if (field.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat) { return payloadField.Value.Children.Count > 1 ? payloadField.Value.Children[1].NumberValue : 0; } return payloadField.Value.NumberValue; } private static AetheriaRuntimeBehaviorPayload FindTypedBehaviorPayload(AetheriaRuntimeCatalogItem typedItem, BehaviorFilter behaviorFilter) { if (typedItem == null) { return null; } return typedItem.BehaviorPayloads.FirstOrDefault(payload => TypedBehaviorMatches(payload, behaviorFilter)); } private static bool HasTypedBehavior(AetheriaRuntimeCatalogItem typedItem, BehaviorFilter behaviorFilter) { return typedItem?.BehaviorPayloads.Any(payload => TypedBehaviorMatches(payload, behaviorFilter)) ?? false; } private static bool TypedBehaviorMatches(AetheriaRuntimeBehaviorPayload payload, BehaviorFilter behaviorFilter) { return AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(payload.Kind, behaviorFilter.Kind); } private sealed class BehaviorFilter { public BehaviorFilter(ItemFilter filter, AetheriaRuntimeBehaviorMetadata metadata) { Filter = filter ?? throw new ArgumentNullException(nameof(filter)); Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); } public ItemFilter Filter { get; } public AetheriaRuntimeBehaviorMetadata Metadata { get; } public string Kind => Metadata.Kind; } private sealed class TradeRow { private readonly ItemManager _itemManager; public TradeRow(ItemInstance item, AetheriaRuntimeCatalogItem typedItem, ItemManager itemManager) { Item = item; TypedItem = typedItem; _itemManager = itemManager; } public ItemInstance Item { get; } public AetheriaRuntimeCatalogItem TypedItem { get; } 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; public int Price { get { if (Item is CraftedItemInstance craftedItemInstance) return TypedItem != null ? (int)(_itemManager.GameplaySettings.QualityPriceModifier.Evaluate(craftedItemInstance.Quality) * TypedItem.Price) : 0; return TypedItem != null ? TypedItem.Price : 0; } } public int ShapeWidth => TypedItem != null && TypedItem.ShapeWidth > 0 ? TypedItem.ShapeWidth : 0; public int ShapeHeight => TypedItem != null && TypedItem.ShapeHeight > 0 ? TypedItem.ShapeHeight : 0; public bool IsHull => !string.IsNullOrWhiteSpace(TypedItem?.HullType); public bool TryGetTypedSimpleCommodityCategory(out SimpleCommodityCategory category) { category = SimpleCommodityCategory.Minerals; return !string.IsNullOrWhiteSpace(TypedItem?.SimpleCommodityCategory) && Enum.TryParse(TypedItem.SimpleCommodityCategory, true, out category); } public bool TryGetTypedCompoundCommodityCategory(out CompoundCommodityCategory category) { category = CompoundCommodityCategory.Wearables; return !string.IsNullOrWhiteSpace(TypedItem?.CompoundCommodityCategory) && Enum.TryParse(TypedItem.CompoundCommodityCategory, true, out category); } public bool TryGetTypedHardpoint(out HardpointType hardpointType) { hardpointType = HardpointType.Hull; return !string.IsNullOrWhiteSpace(TypedItem?.HardpointType) && Enum.TryParse(TypedItem.HardpointType, true, out hardpointType); } } private void UpdateCreditsLabel() { if (CreditsLabel != null) { CreditsLabel.text = GameManager.Credits.ToString("N0"); } } private void Buy(CraftedItemInstance item) { var typedItem = FindTypedTradeItem(item); if (typedItem == null) { ShowUnableToBuy("Missing typed trade row!"); return; } var price = GetTypedTradePrice(item, typedItem); var isShipHull = !string.IsNullOrWhiteSpace(typedItem?.HullType); if (isShipHull && !string.Equals(typedItem.HullType, nameof(HullType.Ship), StringComparison.Ordinal)) { ShowUnableToBuy("Unsupported hull purchase!"); return; } if (price > GameManager.Credits) { ShowUnableToBuy("Insufficient Credits!"); return; } if (!GameManager.CommitTradePurchase(Inventory, _targetCargo, item, price, isShipHull)) { ShowUnableToBuy(isShipHull ? "Unable to create ship!" : "Insufficient Cargo Space!"); return; } UpdateCreditsLabel(); } private void Buy(SimpleCommodity simpleCommodity, int quantity) { var typedItem = FindTypedTradeItem(simpleCommodity); if (typedItem == null) { ShowUnableToBuy("Missing typed trade row!"); return; } var price = typedItem.Price; var clampedQuantity = min(quantity, simpleCommodity.Quantity); var totalPrice = (long)clampedQuantity * price; if (totalPrice > GameManager.Credits) { ShowUnableToBuy("Insufficient Credits!"); return; } if (!GameManager.CommitTradePurchase(Inventory, _targetCargo, simpleCommodity, clampedQuantity, price)) { ShowUnableToBuy("Insufficient Cargo Space!"); return; } UpdateCreditsLabel(); } private int GetTypedTradePrice(CraftedItemInstance item, AetheriaRuntimeCatalogItem typedItem) { return (int)(GameManager.ItemManager.GameplaySettings.QualityPriceModifier.Evaluate(item.Quality) * typedItem.Price); } private void ShowUnableToBuy(string reason) { Dialog.Clear(); Dialog.Title.text = $"Unable to buy: {reason}"; Dialog.Show(); Dialog.MoveToCursor(); } private void ShowBuyQuantityDialog(string itemName, int price, SimpleCommodity simpleCommodity) { int quantity = 1; Dialog.Clear(); Dialog.Title.text = $"Buying {itemName}"; Dialog.AddField( "Quantity", () => quantity, q => quantity = min(min(q, GameManager.Credits / price), simpleCommodity.Quantity)); Dialog.Show(() => { Buy(simpleCommodity, quantity); Populate(); }); Dialog.MoveToCursor(); } private void RenderTradeItemDetailsSurface(AetheriaRuntimeCatalogItem item) { if (item == null) return; var document = ResolveTradeItemDetailsSurfaceDocument(); document.gameObject.SetActive(true); var root = document.rootVisualElement; root.Clear(); root.style.flexGrow = 1; root.style.position = Position.Absolute; root.style.left = 0; root.style.top = 0; root.style.right = 0; root.style.bottom = 0; root.style.alignItems = Align.FlexStart; root.style.justifyContent = Justify.FlexStart; root.style.paddingTop = 16; root.style.paddingLeft = 16; root.pickingMode = PickingMode.Ignore; var shell = new VisualElement(); shell.style.width = 420; shell.style.maxWidth = 520; shell.style.backgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.94f); shell.style.borderTopLeftRadius = 8; shell.style.borderTopRightRadius = 8; shell.style.borderBottomLeftRadius = 8; shell.style.borderBottomRightRadius = 8; shell.style.paddingLeft = 18; shell.style.paddingRight = 18; shell.style.paddingTop = 18; shell.style.paddingBottom = 18; shell.style.borderLeftWidth = 1; shell.style.borderRightWidth = 1; shell.style.borderTopWidth = 1; shell.style.borderBottomWidth = 1; shell.style.borderLeftColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderRightColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderTopColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderBottomColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.pickingMode = PickingMode.Position; root.Add(shell); var lowerer = new EveUiToolkitSurfaceLowerer(); shell.Add(lowerer.Lower(BuildTradeItemDetailsSurfaceDefinition(item), HandleTradeItemDetailsSurfaceCommand)); } private void HandleTradeItemDetailsSurfaceCommand(EveSurfaceCommandRequest request) { if (string.Equals(request.Command, CloseTradeItemDetailsCommand, StringComparison.Ordinal)) { HideTradeItemDetailsSurface(); return; } Debug.LogWarning($"Unknown trade item details command: {request.Command}"); } private void HideTradeItemDetailsSurface() { if (_tradeItemSurfaceDocument == null) return; _tradeItemSurfaceDocument.rootVisualElement.Clear(); _tradeItemSurfaceDocument.gameObject.SetActive(false); } private UIDocument ResolveTradeItemDetailsSurfaceDocument() { if (_tradeItemSurfaceDocument != null) return _tradeItemSurfaceDocument; var host = new GameObject("Aetheria Trade Item Details Surface"); host.transform.SetParent(transform, false); var document = host.AddComponent(); document.sortingOrder = 1003; host.SetActive(false); _tradeItemSurfaceDocument = document; return document; } private EveSurfaceDocument BuildTradeItemDetailsSurfaceDefinition(AetheriaRuntimeCatalogItem item) { var children = new List { Card( $"{TradeItemSurfaceId}.summary", item.Name, Text($"{TradeItemSurfaceId}.description", item.Description ?? "No typed item description is available."), Text( $"{TradeItemSurfaceId}.note", "Trade still owns purchase and row population. This surface replaces the old PropertiesPanel item-inspection shell."), Metric( $"{TradeItemSurfaceId}.manufacturer", "Manufacturer", ActionGameManager.RuntimeCatalog?.GetManufacturer(item)?.Name ?? "GameCult"), Metric( $"{TradeItemSurfaceId}.mass", "Mass", ActionGameManager.RuntimePlayerSettings.Format((float)item.Mass)), Metric( $"{TradeItemSurfaceId}.price", "Price", item.Price.ToString("N0"))) }; if (!string.IsNullOrWhiteSpace(item.HardpointType)) { children.Add(Card( $"{TradeItemSurfaceId}.durability.card", "Durability", Metric( $"{TradeItemSurfaceId}.durability", "Max Durability", ActionGameManager.RuntimePlayerSettings.Format((float)item.Durability)), Metric( $"{TradeItemSurfaceId}.temperature_range", "Thermal Range", FormatTemperatureRange(item)))); foreach (var behaviorCard in BuildTradeItemBehaviorCards(item)) { children.Add(behaviorCard); } } children.Add(ButtonRow( $"{TradeItemSurfaceId}.actions", Button($"{TradeItemSurfaceId}.close", "Close", CloseTradeItemDetailsCommand))); return new EveSurfaceDocument( CargoSelectorSurfaceType, CargoSelectorSurfaceSchema, CargoSelectorSurfaceProviderId, CargoSelectorSurfaceProviderKind, "Trade Item Details", version: 1, DateTime.UtcNow.ToString("O"), new EveSurfaceTree( TradeItemSurfaceId, Node( $"{TradeItemSurfaceId}.root", "surface", Array.Empty<(string Key, string Value)>(), children.ToArray()), Array.Empty()), new[] { new EveCommandTemplate(CloseTradeItemDetailsCommand, "Close", "unity-uitoolkit") }); } private IEnumerable BuildTradeItemBehaviorCards(AetheriaRuntimeCatalogItem item) { foreach (var behavior in item.BehaviorPayloads ?? Array.Empty()) { if (string.Equals(behavior.Kind, AetheriaRuntimeBehaviorKinds.StatModifier, StringComparison.Ordinal)) { var statReference = ReadTypedStatReference(FindTypedBehaviorField(behavior, 1)?.Value); var modifier = ReadTypedPerformanceStat(FindTypedBehaviorField(behavior, 2)?.Value); var modifierType = ReadTypedEnum(FindTypedBehaviorField(behavior, 3)?.Value, StatModifierType.Constant); yield return Card( $"{TradeItemSurfaceId}.behavior.{behavior.Kind}.stat_modifier", "Stat Modifier", Metric( $"{TradeItemSurfaceId}.behavior.{behavior.Kind}.target", $"{statReference.target.SplitCamelCase()}:{statReference.stat.SplitCamelCase()}", $"{(modifierType == StatModifierType.Constant ? "+" : "x")}{ActionGameManager.RuntimePlayerSettings.Format(modifier.Min)}")); continue; } var metadata = AetheriaRuntimeBehaviorMetadataCatalog.Get(behavior.Kind); if (metadata == null) continue; var fields = metadata.DisplayFields .Select(field => BuildTradeItemBehaviorMetric(behavior, field)) .Where(metric => metric != null) .ToArray(); if (fields.Length == 0) continue; yield return Card( $"{TradeItemSurfaceId}.behavior.{behavior.Kind}", behavior.Kind.FormatTypeName(), fields); } } private EveSurfaceComponent BuildTradeItemBehaviorMetric( AetheriaRuntimeBehaviorPayload behavior, AetheriaRuntimeBehaviorFieldMetadata field) { var payloadField = FindTypedBehaviorField(behavior, field.Key); if (payloadField == null) return null; string value; switch (field.ValueKind) { case AetheriaRuntimeBehaviorFieldValueKind.Number: value = ActionGameManager.RuntimePlayerSettings.Format((float)payloadField.Value.NumberValue); break; case AetheriaRuntimeBehaviorFieldValueKind.Temperature: value = ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)payloadField.Value.NumberValue); break; case AetheriaRuntimeBehaviorFieldValueKind.Integer: value = ((int)payloadField.Value.NumberValue).ToString(); break; case AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat: value = ActionGameManager.RuntimePlayerSettings.Format(ReadTypedPerformanceStat(payloadField.Value).Min); break; default: return null; } return Metric( $"{TradeItemSurfaceId}.behavior.{behavior.Kind}.{field.Key}", field.Name.SplitCamelCase(), value); } private static string FormatTemperatureRange(AetheriaRuntimeCatalogItem item) { if (item.MaximumTemperature > item.MinimumTemperature) { return $"{ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)item.MinimumTemperature)} to " + $"{ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)item.MaximumTemperature)}"; } return "No typed thermal range"; } private static AetheriaRuntimeBehaviorField FindTypedBehaviorField(AetheriaRuntimeBehaviorPayload behavior, int? key) { return key == null ? null : behavior.Fields.FirstOrDefault(field => field.Key == key.Value); } private static PerformanceStat ReadTypedPerformanceStat(AetheriaRuntimeBehaviorValue value) { return new PerformanceStat { Min = ReadTypedChildNumber(value, 0), Max = ReadTypedChildNumber(value, 1), HeatExponentMultiplier = ReadTypedChildNumber(value, 2), DurabilityExponentMultiplier = ReadTypedChildNumber(value, 3), QualityExponent = ReadTypedChildNumber(value, 4) }; } private static (string target, string stat) ReadTypedStatReference(AetheriaRuntimeBehaviorValue value) { return ( ReadTypedChildString(value, 1), ReadTypedChildString(value, 2)); } private static float ReadTypedChildNumber(AetheriaRuntimeBehaviorValue value, int index) { return value != null && value.Children.Count > index ? (float)value.Children[index].NumberValue : 0f; } private static string ReadTypedChildString(AetheriaRuntimeBehaviorValue value, int index) { return value != null && value.Children.Count > index ? value.Children[index].StringValue ?? "" : ""; } private static T ReadTypedEnum(AetheriaRuntimeBehaviorValue value, T fallback) where T : struct { if (!string.IsNullOrWhiteSpace(value?.StringValue) && Enum.TryParse(value.StringValue, true, out T parsed)) { return parsed; } return value != null && Enum.IsDefined(typeof(T), (int)value.NumberValue) ? (T)Enum.ToObject(typeof(T), (int)value.NumberValue) : fallback; } void Start() { FoldoutButton.onClick.AddListener(() => { RenderCargoSelectorSurface(); }); } void Update() { } private void RenderFilterSurface() { BuildFilterSurfaceCommands(); var document = ResolveFilterSurfaceDocument(); document.gameObject.SetActive(true); var root = document.rootVisualElement; root.Clear(); root.style.flexGrow = 1; root.style.position = Position.Absolute; root.style.left = 0; root.style.top = 0; root.style.right = 0; root.style.bottom = 0; root.style.alignItems = Align.FlexStart; root.style.justifyContent = Justify.FlexStart; root.style.paddingTop = 16; root.style.paddingLeft = 16; root.pickingMode = PickingMode.Ignore; var shell = new VisualElement(); shell.style.width = 420; shell.style.maxWidth = 520; shell.style.backgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.94f); shell.style.borderTopLeftRadius = 8; shell.style.borderTopRightRadius = 8; shell.style.borderBottomLeftRadius = 8; shell.style.borderBottomRightRadius = 8; shell.style.paddingLeft = 18; shell.style.paddingRight = 18; shell.style.paddingTop = 18; shell.style.paddingBottom = 18; shell.style.borderLeftWidth = 1; shell.style.borderRightWidth = 1; shell.style.borderTopWidth = 1; shell.style.borderBottomWidth = 1; shell.style.borderLeftColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderRightColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderTopColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderBottomColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.pickingMode = PickingMode.Position; root.Add(shell); var lowerer = new EveUiToolkitSurfaceLowerer(); shell.Add(lowerer.Lower(BuildFilterSurfaceDefinition(), HandleFilterSurfaceCommand)); } private void BuildFilterSurfaceCommands() { _filterSurfaceCommands.Clear(); foreach (var hardpointType in ((HardpointType[])Enum.GetValues(typeof(HardpointType))) .Where(type => _hardpointFilter.filter == null || type != _hardpointFilter.type)) { var command = $"{FilterSurfaceId}.hardpoint.{hardpointType}"; _filterSurfaceCommands[command] = () => ApplyHardpointFilter(hardpointType); } foreach (var commodityType in ((SimpleCommodityCategory[])Enum.GetValues(typeof(SimpleCommodityCategory))) .Where(type => _commodityFilter.filter == null || type != _commodityFilter.type)) { var command = $"{FilterSurfaceId}.simple.{commodityType}"; _filterSurfaceCommands[command] = () => ApplySimpleCommodityFilter(commodityType); } foreach (var commodityType in ((CompoundCommodityCategory[])Enum.GetValues(typeof(CompoundCommodityCategory))) .Where(type => _compoundCommodityFilter.filter == null || type != _compoundCommodityFilter.type)) { var command = $"{FilterSurfaceId}.compound.{commodityType}"; _filterSurfaceCommands[command] = () => ApplyCompoundCommodityFilter(commodityType); } foreach (var metadata in AetheriaRuntimeBehaviorMetadataCatalog.All .Where(option => _behaviorFilters.All(filter => filter.Kind != option.Kind)) .OrderBy(option => option.Kind, StringComparer.Ordinal)) { var command = $"{FilterSurfaceId}.behavior.{metadata.Kind}"; _filterSurfaceCommands[command] = () => ApplyBehaviorFilter(metadata); } if (!MinimumSizeFilter.gameObject.activeSelf) { _filterSurfaceCommands[$"{FilterSurfaceId}.size.minimum"] = EnableMinimumSizeFilter; } if (!MaximumSizeFilter.gameObject.activeSelf) { _filterSurfaceCommands[$"{FilterSurfaceId}.size.maximum"] = EnableMaximumSizeFilter; } } private void HandleFilterSurfaceCommand(EveSurfaceCommandRequest request) { if (string.Equals(request.Command, CloseFilterSurfaceCommand, StringComparison.Ordinal)) { HideFilterSurface(); return; } if (_filterSurfaceCommands.TryGetValue(request.Command, out var action)) { action(); HideFilterSurface(); return; } Debug.LogWarning($"Unknown trade filter command: {request.Command}"); } private void HideFilterSurface() { if (_filterSurfaceDocument == null) return; _filterSurfaceDocument.rootVisualElement.Clear(); _filterSurfaceDocument.gameObject.SetActive(false); } private UIDocument ResolveFilterSurfaceDocument() { if (_filterSurfaceDocument != null) return _filterSurfaceDocument; var host = new GameObject("Aetheria Trade Filter Surface"); host.transform.SetParent(transform, false); var document = host.AddComponent(); document.sortingOrder = 1001; host.SetActive(false); _filterSurfaceDocument = document; return document; } private EveSurfaceDocument BuildFilterSurfaceDefinition() { var cards = new List { Card( $"{FilterSurfaceId}.summary", "Trade Filters", Text( $"{FilterSurfaceId}.note", "Trade still owns filter state and population. This surface just removes the old context-menu shell."), Text( $"{FilterSurfaceId}.active", BuildFilterSummary())) }; var hardpointButtons = _filterSurfaceCommands.Keys .Where(command => command.StartsWith($"{FilterSurfaceId}.hardpoint.", StringComparison.Ordinal)) .OrderBy(command => command, StringComparer.Ordinal) .Select(command => Button( command, command.Split('.').Last().FormatTypeName(), command)) .ToArray(); if (hardpointButtons.Length > 0) { cards.Add(Card($"{FilterSurfaceId}.hardpoint.card", "Gear Type", ButtonColumn($"{FilterSurfaceId}.hardpoint.options", hardpointButtons))); } var simpleButtons = _filterSurfaceCommands.Keys .Where(command => command.StartsWith($"{FilterSurfaceId}.simple.", StringComparison.Ordinal)) .OrderBy(command => command, StringComparer.Ordinal) .Select(command => Button( command, command.Split('.').Last().FormatTypeName(), command)) .ToArray(); if (simpleButtons.Length > 0) { cards.Add(Card($"{FilterSurfaceId}.simple.card", "Simple Commodity", ButtonColumn($"{FilterSurfaceId}.simple.options", simpleButtons))); } var compoundButtons = _filterSurfaceCommands.Keys .Where(command => command.StartsWith($"{FilterSurfaceId}.compound.", StringComparison.Ordinal)) .OrderBy(command => command, StringComparer.Ordinal) .Select(command => Button( command, command.Split('.').Last().FormatTypeName(), command)) .ToArray(); if (compoundButtons.Length > 0) { cards.Add(Card($"{FilterSurfaceId}.compound.card", "Compound Commodity", ButtonColumn($"{FilterSurfaceId}.compound.options", compoundButtons))); } var behaviorButtons = _filterSurfaceCommands.Keys .Where(command => command.StartsWith($"{FilterSurfaceId}.behavior.", StringComparison.Ordinal)) .OrderBy(command => command, StringComparer.Ordinal) .Select(command => Button( command, command.Split('.').Last().FormatTypeName(), command)) .ToArray(); if (behaviorButtons.Length > 0) { cards.Add(Card($"{FilterSurfaceId}.behavior.card", "Item Behavior", ButtonColumn($"{FilterSurfaceId}.behavior.options", behaviorButtons))); } var sizeButtons = _filterSurfaceCommands.Keys .Where(command => command.StartsWith($"{FilterSurfaceId}.size.", StringComparison.Ordinal)) .OrderBy(command => command, StringComparer.Ordinal) .Select(command => Button( command, command.EndsWith(".minimum", StringComparison.Ordinal) ? "Minimum Size" : "Maximum Size", command)) .ToArray(); if (sizeButtons.Length > 0) { cards.Add(Card($"{FilterSurfaceId}.size.card", "Size", ButtonColumn($"{FilterSurfaceId}.size.options", sizeButtons))); } cards.Add(ButtonRow($"{FilterSurfaceId}.actions", Button($"{FilterSurfaceId}.close", "Close", CloseFilterSurfaceCommand))); return new EveSurfaceDocument( CargoSelectorSurfaceType, CargoSelectorSurfaceSchema, CargoSelectorSurfaceProviderId, CargoSelectorSurfaceProviderKind, "Trade Filter Selector", version: 1, DateTime.UtcNow.ToString("O"), new EveSurfaceTree( FilterSurfaceId, Node( $"{FilterSurfaceId}.root", "surface", Array.Empty<(string Key, string Value)>(), cards.ToArray()), Array.Empty()), _filterSurfaceCommands.Keys .Select(command => new EveCommandTemplate(command, command.Split('.').Last().FormatTypeName(), "unity-uitoolkit")) .Append(new EveCommandTemplate(CloseFilterSurfaceCommand, "Close", "unity-uitoolkit")) .ToArray()); } private void RenderRowActionSurface(string title, params (string Label, Action Action)[] actions) { _rowActionTitle = title; BuildRowActionSurfaceCommands(actions); var document = ResolveRowActionSurfaceDocument(); document.gameObject.SetActive(true); var root = document.rootVisualElement; root.Clear(); root.style.flexGrow = 1; root.style.position = Position.Absolute; root.style.left = 0; root.style.top = 0; root.style.right = 0; root.style.bottom = 0; root.style.alignItems = Align.FlexStart; root.style.justifyContent = Justify.FlexStart; root.style.paddingTop = 16; root.style.paddingLeft = 16; root.pickingMode = PickingMode.Ignore; var shell = new VisualElement(); shell.style.width = 320; shell.style.maxWidth = 360; shell.style.backgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.94f); shell.style.borderTopLeftRadius = 8; shell.style.borderTopRightRadius = 8; shell.style.borderBottomLeftRadius = 8; shell.style.borderBottomRightRadius = 8; shell.style.paddingLeft = 18; shell.style.paddingRight = 18; shell.style.paddingTop = 18; shell.style.paddingBottom = 18; shell.style.borderLeftWidth = 1; shell.style.borderRightWidth = 1; shell.style.borderTopWidth = 1; shell.style.borderBottomWidth = 1; shell.style.borderLeftColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderRightColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderTopColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderBottomColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.pickingMode = PickingMode.Position; root.Add(shell); var lowerer = new EveUiToolkitSurfaceLowerer(); shell.Add(lowerer.Lower(BuildRowActionSurfaceDefinition(), HandleRowActionSurfaceCommand)); } private void BuildRowActionSurfaceCommands(IEnumerable<(string Label, Action Action)> actions) { _rowActionSurfaceCommands.Clear(); foreach (var actionEntry in actions.Select((entry, index) => (entry, index))) { var command = $"{RowActionSurfaceId}.action_{actionEntry.index}"; _rowActionSurfaceCommands[command] = actionEntry.entry.Action; } } private void HandleRowActionSurfaceCommand(EveSurfaceCommandRequest request) { if (string.Equals(request.Command, CloseRowActionSurfaceCommand, StringComparison.Ordinal)) { HideRowActionSurface(); return; } if (_rowActionSurfaceCommands.TryGetValue(request.Command, out var action)) { action(); HideRowActionSurface(); return; } Debug.LogWarning($"Unknown trade row action command: {request.Command}"); } private void HideRowActionSurface() { if (_rowActionSurfaceDocument == null) return; _rowActionSurfaceDocument.rootVisualElement.Clear(); _rowActionSurfaceDocument.gameObject.SetActive(false); } private UIDocument ResolveRowActionSurfaceDocument() { if (_rowActionSurfaceDocument != null) return _rowActionSurfaceDocument; var host = new GameObject("Aetheria Trade Row Action Surface"); host.transform.SetParent(transform, false); var document = host.AddComponent(); document.sortingOrder = 1002; host.SetActive(false); _rowActionSurfaceDocument = document; return document; } private EveSurfaceDocument BuildRowActionSurfaceDefinition() { var commands = _rowActionSurfaceCommands.Keys.OrderBy(command => command, StringComparer.Ordinal).ToArray(); var buttons = commands .Select(command => Button(command, command.EndsWith("action_0", StringComparison.Ordinal) ? "Buy Quantity" : command.Split('.').Last(), command)) .ToArray(); return new EveSurfaceDocument( CargoSelectorSurfaceType, CargoSelectorSurfaceSchema, CargoSelectorSurfaceProviderId, CargoSelectorSurfaceProviderKind, "Trade Row Actions", version: 1, DateTime.UtcNow.ToString("O"), new EveSurfaceTree( RowActionSurfaceId, Node( $"{RowActionSurfaceId}.root", "surface", Array.Empty<(string Key, string Value)>(), Card( $"{RowActionSurfaceId}.card", "Trade Action", Text($"{RowActionSurfaceId}.title", _rowActionTitle), Text( $"{RowActionSurfaceId}.note", "Trade still owns the quantity dialog and purchase commit. This surface just removes the old right-click context-menu shell."), ButtonColumn($"{RowActionSurfaceId}.options", buttons), ButtonRow($"{RowActionSurfaceId}.actions", Button($"{RowActionSurfaceId}.close", "Close", CloseRowActionSurfaceCommand)))), Array.Empty()), commands .Select(command => new EveCommandTemplate(command, command.EndsWith("action_0", StringComparison.Ordinal) ? "Buy Quantity" : command.Split('.').Last(), "unity-uitoolkit")) .Append(new EveCommandTemplate(CloseRowActionSurfaceCommand, "Close", "unity-uitoolkit")) .ToArray()); } private void ApplyHardpointFilter(HardpointType hardpointType) { EnsureHardpointFilter(); ClearCommodityFilters(); _hardpointFilter.filter.Label.text = $"Hardpoint: {Enum.GetName(typeof(HardpointType), hardpointType)}"; _hardpointFilter.type = hardpointType; Populate(); } private void ApplySimpleCommodityFilter(SimpleCommodityCategory category) { EnsureCommodityFilter(); ClearHardpointFilter(); ClearCompoundCommodityFilter(); _commodityFilter.filter.Label.text = $"Simple Commodity: {Enum.GetName(typeof(SimpleCommodityCategory), category)}"; _commodityFilter.type = category; Populate(); } private void ApplyCompoundCommodityFilter(CompoundCommodityCategory category) { EnsureCompoundCommodityFilter(); ClearHardpointFilter(); ClearCommodityFilter(); _compoundCommodityFilter.filter.Label.text = $"Compound Commodity: {Enum.GetName(typeof(CompoundCommodityCategory), category)}"; _compoundCommodityFilter.type = category; Populate(); } private void ApplyBehaviorFilter(AetheriaRuntimeBehaviorMetadata metadata) { var matchingType = _behaviorFilters.FirstOrDefault(filter => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(filter.Kind, metadata.Kind) || AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(metadata.Kind, filter.Kind)); if (matchingType?.Filter != null) { matchingType.Filter.DisableButton.onClick.Invoke(); } var filter = FilterPrototype.Instantiate(); filter.Label.text = metadata.Kind.FormatTypeName(); var behaviorFilter = new BehaviorFilter(filter, metadata); filter.OnDisable += () => { _behaviorFilters.Remove(behaviorFilter); Populate(); }; _behaviorFilters.Add(behaviorFilter); Populate(); } private void EnableMinimumSizeFilter() { MinimumSizeFilter.gameObject.SetActive(true); MinimumSizeFilter.OnDisable += () => Populate(); Populate(); } private void EnableMaximumSizeFilter() { MaximumSizeFilter.gameObject.SetActive(true); MaximumSizeFilter.OnDisable += () => Populate(); Populate(); } private void EnsureHardpointFilter() { if (_hardpointFilter.filter != null) return; _hardpointFilter.filter = FilterPrototype.Instantiate(); _hardpointFilter.filter.OnDisable += () => { _hardpointFilter.filter = null; Populate(); }; } private void EnsureCommodityFilter() { if (_commodityFilter.filter != null) return; _commodityFilter.filter = FilterPrototype.Instantiate(); _commodityFilter.filter.OnDisable += () => { _commodityFilter.filter = null; Populate(); }; } private void EnsureCompoundCommodityFilter() { if (_compoundCommodityFilter.filter != null) return; _compoundCommodityFilter.filter = FilterPrototype.Instantiate(); _compoundCommodityFilter.filter.OnDisable += () => { _compoundCommodityFilter.filter = null; Populate(); }; } private void ClearHardpointFilter() { if (_hardpointFilter.filter == null) return; _hardpointFilter.filter.GetComponent().ReturnToPool(); _hardpointFilter.filter = null; } private void ClearCommodityFilter() { if (_commodityFilter.filter == null) return; _commodityFilter.filter.GetComponent().ReturnToPool(); _commodityFilter.filter = null; } private void ClearCompoundCommodityFilter() { if (_compoundCommodityFilter.filter == null) return; _compoundCommodityFilter.filter.GetComponent().ReturnToPool(); _compoundCommodityFilter.filter = null; } private void ClearCommodityFilters() { ClearCommodityFilter(); ClearCompoundCommodityFilter(); } private string BuildFilterSummary() { var activeFilters = new List(); if (_hardpointFilter.filter != null) { activeFilters.Add($"Gear: {Enum.GetName(typeof(HardpointType), _hardpointFilter.type)}"); } if (_commodityFilter.filter != null) { activeFilters.Add($"Simple: {Enum.GetName(typeof(SimpleCommodityCategory), _commodityFilter.type)}"); } if (_compoundCommodityFilter.filter != null) { activeFilters.Add($"Compound: {Enum.GetName(typeof(CompoundCommodityCategory), _compoundCommodityFilter.type)}"); } activeFilters.AddRange(_behaviorFilters.Select(filter => $"Behavior: {filter.Kind.FormatTypeName()}")); if (MinimumSizeFilter.gameObject.activeSelf) { activeFilters.Add("Minimum size active"); } if (MaximumSizeFilter.gameObject.activeSelf) { activeFilters.Add("Maximum size active"); } return activeFilters.Count == 0 ? "No active filters" : string.Join(" | ", activeFilters); } private void RenderCargoSelectorSurface() { BuildCargoSelectionCommands(); var document = ResolveCargoSelectorSurfaceDocument(); document.gameObject.SetActive(true); var root = document.rootVisualElement; root.Clear(); root.style.flexGrow = 1; root.style.position = Position.Absolute; root.style.left = 0; root.style.top = 0; root.style.right = 0; root.style.bottom = 0; root.style.alignItems = Align.FlexEnd; root.style.justifyContent = Justify.FlexStart; root.style.paddingTop = 16; root.style.paddingRight = 16; root.pickingMode = PickingMode.Ignore; var shell = new VisualElement(); shell.style.width = 360; shell.style.maxWidth = 420; shell.style.backgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.94f); shell.style.borderTopLeftRadius = 8; shell.style.borderTopRightRadius = 8; shell.style.borderBottomLeftRadius = 8; shell.style.borderBottomRightRadius = 8; shell.style.paddingLeft = 18; shell.style.paddingRight = 18; shell.style.paddingTop = 18; shell.style.paddingBottom = 18; shell.style.borderLeftWidth = 1; shell.style.borderRightWidth = 1; shell.style.borderTopWidth = 1; shell.style.borderBottomWidth = 1; shell.style.borderLeftColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderRightColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderTopColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.style.borderBottomColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); shell.pickingMode = PickingMode.Position; root.Add(shell); var lowerer = new EveUiToolkitSurfaceLowerer(); shell.Add(lowerer.Lower(BuildCargoSelectorSurfaceDefinition(), HandleCargoSelectorSurfaceCommand)); } private void BuildCargoSelectionCommands() { _cargoSelectionCommands.Clear(); if (GameManager.DockingBay != null && _targetCargo != GameManager.DockingBay) { _cargoSelectionCommands["aetheria.trade.target_cargo_selector.docking_bay"] = (GameManager.DockingBay, "Docking Bay"); } if (GameManager.CurrentEntity?.Parent == null) return; foreach (var ship in GameManager.CurrentEntity.Parent.Children .Where(entity => entity is Ship { IsPlayerShip: true }) .Cast() .Select((ship, shipIndex) => (ship, shipIndex))) { foreach (var bay in ship.ship.CargoBays.Select((cargoBay, index) => (cargoBay, index))) { if (_targetCargo == bay.cargoBay) continue; var command = $"aetheria.trade.target_cargo_selector.ship_{ship.shipIndex}_bay_{bay.index}"; _cargoSelectionCommands[command] = (bay.cargoBay, $"{ship.ship.Name} Bay {bay.index + 1}"); } } } private void HandleCargoSelectorSurfaceCommand(EveSurfaceCommandRequest request) { if (string.Equals(request.Command, CloseCargoSelectorCommand, StringComparison.Ordinal)) { HideCargoSelectorSurface(); return; } if (_cargoSelectionCommands.TryGetValue(request.Command, out var option)) { _targetCargo = option.Cargo; TargetCargoLabel.text = option.Label; HideCargoSelectorSurface(); Populate(); return; } Debug.LogWarning($"Unknown trade cargo selector command: {request.Command}"); } private void HideCargoSelectorSurface() { if (_cargoSelectorSurfaceDocument == null) return; _cargoSelectorSurfaceDocument.rootVisualElement.Clear(); _cargoSelectorSurfaceDocument.gameObject.SetActive(false); } private UIDocument ResolveCargoSelectorSurfaceDocument() { if (_cargoSelectorSurfaceDocument != null) return _cargoSelectorSurfaceDocument; var host = new GameObject("Aetheria Trade Cargo Selector Surface"); host.transform.SetParent(transform, false); var document = host.AddComponent(); document.sortingOrder = 1000; host.SetActive(false); _cargoSelectorSurfaceDocument = document; return document; } private EveSurfaceDocument BuildCargoSelectorSurfaceDefinition() { var buttons = _cargoSelectionCommands .OrderBy(pair => pair.Value.Label, StringComparer.Ordinal) .Select(pair => Button( $"{CargoSelectorSurfaceId}.{pair.Key.Split('.').Last()}", pair.Value.Label, pair.Key)) .ToArray(); return new EveSurfaceDocument( CargoSelectorSurfaceType, CargoSelectorSurfaceSchema, CargoSelectorSurfaceProviderId, CargoSelectorSurfaceProviderKind, "Trade Target Cargo Selector", version: 1, DateTime.UtcNow.ToString("O"), new EveSurfaceTree( CargoSelectorSurfaceId, Node( $"{CargoSelectorSurfaceId}.root", "surface", Array.Empty<(string Key, string Value)>(), Card( $"{CargoSelectorSurfaceId}.card", "Target Cargo", Metric($"{CargoSelectorSurfaceId}.current", "Current", TargetCargoLabel.text ?? ""), Text( $"{CargoSelectorSurfaceId}.note", "Trade still owns local presentation state here; this surface cuts out the old context-menu option list."), ButtonColumn($"{CargoSelectorSurfaceId}.options", buttons), ButtonRow( $"{CargoSelectorSurfaceId}.actions", Button($"{CargoSelectorSurfaceId}.close", "Close", CloseCargoSelectorCommand)))), Array.Empty()), _cargoSelectionCommands.Keys .Select(command => new EveCommandTemplate(command, _cargoSelectionCommands[command].Label, "unity-uitoolkit")) .Append(new EveCommandTemplate(CloseCargoSelectorCommand, "Close", "unity-uitoolkit")) .ToArray()); } private static EveSurfaceComponent Card( string id, string title, params EveSurfaceComponent[] children) { return Node(id, "card", new[] { ("title", title) }, children); } private static EveSurfaceComponent Metric(string id, string label, string value) { return Node(id, "metric", new[] { ("label", label), ("value", value) }); } private static EveSurfaceComponent Text(string id, string value) { return Node(id, "text", new[] { ("value", value) }); } private static EveSurfaceComponent Button(string id, string label, string command) { return Node(id, "control.button", new[] { ("label", label), ("command", command) }); } private static EveSurfaceComponent ButtonRow( string id, params EveSurfaceComponent[] children) { return Node(id, "row", Array.Empty<(string Key, string Value)>(), children); } private static EveSurfaceComponent ButtonColumn( string id, params EveSurfaceComponent[] children) { return Node(id, "column", Array.Empty<(string Key, string Value)>(), children); } private static EveSurfaceComponent Node( string id, string kind, IEnumerable<(string Key, string Value)> props, params EveSurfaceComponent[] children) { return new EveSurfaceComponent( id, kind, props.ToDictionary(prop => prop.Key, prop => prop.Value, StringComparer.Ordinal), children ?? Array.Empty()); } private void OnDisable() { HideCargoSelectorSurface(); HideFilterSurface(); HideRowActionSurface(); HideTradeItemDetailsSurface(); } private void OnDestroy() { if (_cargoSelectorSurfaceDocument != null) { Destroy(_cargoSelectorSurfaceDocument.gameObject); _cargoSelectorSurfaceDocument = null; } if (_filterSurfaceDocument != null) { Destroy(_filterSurfaceDocument.gameObject); _filterSurfaceDocument = null; } if (_rowActionSurfaceDocument != null) { Destroy(_rowActionSurfaceDocument.gameObject); _rowActionSurfaceDocument = null; } if (_tradeItemSurfaceDocument != null) { Destroy(_tradeItemSurfaceDocument.gameObject); _tradeItemSurfaceDocument = null; } } }