Move trade cargo selector onto Eve surfaces

This commit is contained in:
Meta Vens 2026-06-16 21:45:56 +01:00
commit 74ed97a76f
3 changed files with 304 additions and 27 deletions

View file

@ -36,6 +36,7 @@ RequireMainMenuSettingsShellUsesEveSurface(root);
RequireSectorMapZoneDetailsUseEveSurface(root);
RequireRuntimeMenuTabsUseEveSurface(root);
RequireInventoryShipSettingsUseEveSurface(root);
RequireTradeCargoSelectorUseEveSurface(root);
RequirePlayerSettingsEveSurface(root);
RequireMainMenuContinueRunState(root);
RequirePropertiesPanelReadOnlyInspector(root);
@ -504,6 +505,7 @@ Console.WriteLine("Main-menu settings shell: settings/input/audio subpages lower
Console.WriteLine("Sector-map zone details shell: zone inspection lowers through an Eve UI Toolkit surface instead of PropertiesPanel rows");
Console.WriteLine("Runtime menu tab shell: tab navigation lowers through an Eve UI Toolkit surface instead of MenuTabButton click wiring");
Console.WriteLine("Inventory ship-settings shell: background ship tuning lowers through an Eve UI Toolkit surface instead of PropertiesPanel.AddField");
Console.WriteLine("Trade cargo-selector shell: target cargo selection lowers through an Eve UI Toolkit surface instead of ContextMenu.AddOption");
Console.WriteLine("Main-menu Continue authority: Continue selects typed run state instead of a null button");
Console.WriteLine("PropertiesPanel inspector authority: dead generic reflection inspector path is deleted");
Console.WriteLine("Typed behavior metadata authority: live heat/mining/thermotoggle payload kinds stay owned by package metadata");
@ -2643,6 +2645,56 @@ static void RequireInventoryShipSettingsUseEveSurface(string root)
}
}
static void RequireTradeCargoSelectorUseEveSurface(string root)
{
var tradeMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "TradeMenu.cs");
if (!File.Exists(tradeMenuPath))
{
throw new InvalidOperationException("Cannot verify trade cargo-selector shell; TradeMenu.cs is missing.");
}
var source = File.ReadAllText(tradeMenuPath);
var requiredSymbols = new[]
{
"CargoSelectorSurfaceId",
"RenderCargoSelectorSurface(",
"BuildCargoSelectionCommands(",
"HandleCargoSelectorSurfaceCommand(",
"ResolveCargoSelectorSurfaceDocument(",
"BuildCargoSelectorSurfaceDefinition(",
"CloseCargoSelectorCommand",
"new EveUiToolkitSurfaceLowerer()"
};
var missingSymbols = requiredSymbols
.Where(symbol => !source.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingSymbols.Length > 0)
{
throw new InvalidOperationException(
"TradeMenu no longer lowers the cargo selector through an Eve surface shell: " +
string.Join(", ", missingSymbols));
}
var forbiddenSymbols = new[]
{
"ContextMenu.AddOption(\"Docking Bay\"",
"ContextMenu.AddOption($\"{ship.Name} Bay {bay.index+1}\""
};
var hits = forbiddenSymbols
.Where(symbol => source.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (hits.Length > 0)
{
throw new InvalidOperationException(
"TradeMenu still owns cargo selection through the old context-menu option path: " +
string.Join(", ", hits));
}
}
static void RequirePlayerSettingsEveSurface(string root)
{
var projectorPath = Path.Combine(root, "Aetheria.State", "AetheriaPlayerSettingsSurfaceProjector.cs");

View file

@ -3,25 +3,35 @@ 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";
public ActionGameManager GameManager;
public ContextMenu ContextMenu;
public ConfirmationDialog Dialog;
public Button NewFilterButton;
public UnityEngine.UI.Button NewFilterButton;
public Prototype FilterPrototype;
public SizeFilter MinimumSizeFilter;
public SizeFilter MaximumSizeFilter;
public PropertiesPanel Properties;
public Spreadsheet Spreadsheet;
public TextMeshProUGUI TargetCargoLabel;
public Button FoldoutButton;
public UnityEngine.UI.Button FoldoutButton;
public TextMeshProUGUI CreditsLabel;
private EquippedCargoBay _targetCargo;
@ -29,6 +39,9 @@ public class TradeMenu : MonoBehaviour
private (ItemFilter filter, SimpleCommodityCategory type) _commodityFilter;
private (ItemFilter filter, CompoundCommodityCategory type) _compoundCommodityFilter;
private List<BehaviorFilter> _behaviorFilters = new List<BehaviorFilter>();
private readonly Dictionary<string, (EquippedCargoBay Cargo, string Label)> _cargoSelectionCommands =
new Dictionary<string, (EquippedCargoBay Cargo, string Label)>(StringComparer.Ordinal);
private UIDocument _cargoSelectorSurfaceDocument;
public EquippedCargoBay Inventory { get; set; }
@ -37,6 +50,7 @@ public class TradeMenu : MonoBehaviour
if (GameManager.DockedEntity == null) return;
_targetCargo = GameManager.DockingBay;
TargetCargoLabel.text = "Docking Bay";
HideCargoSelectorSurface();
Properties.GameManager = GameManager;
UpdateCreditsLabel();
@ -604,30 +618,7 @@ public class TradeMenu : MonoBehaviour
{
FoldoutButton.onClick.AddListener(() =>
{
ContextMenu.Clear();
if(_targetCargo != GameManager.DockingBay)
ContextMenu.AddOption("Docking Bay",
() =>
{
_targetCargo = GameManager.DockingBay;
TargetCargoLabel.text = "Docking Bay";
});
foreach (var ship in GameManager.CurrentEntity.Parent.Children.Where(e => e is Ship {IsPlayerShip: true}))
{
foreach (var bay in ship.CargoBays.Select((bay, index) => (bay, index)))
{
if(_targetCargo != bay.bay)
{
ContextMenu.AddOption($"{ship.Name} Bay {bay.index+1}",
() =>
{
_targetCargo = bay.bay;
TargetCargoLabel.text = $"{ship.Name} Bay {bay.index+1}";
});
}
}
}
ContextMenu.Show();
RenderCargoSelectorSurface();
});
}
@ -635,4 +626,230 @@ public class TradeMenu : MonoBehaviour
{
}
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<Ship>()
.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<UIDocument>();
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<EveStyleToken>()),
_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<EveSurfaceComponent>());
}
private void OnDisable()
{
HideCargoSelectorSurface();
}
private void OnDestroy()
{
if (_cargoSelectorSurfaceDocument != null)
{
Destroy(_cargoSelectorSurfaceDocument.gameObject);
_cargoSelectorSurfaceDocument = null;
}
}
}

View file

@ -121,7 +121,8 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
now lowers through a local Eve/UI Toolkit surface instead of wiring old
`MenuTabButton` clicks. Inventory background ship tuning now lowers through a
local Eve/UI Toolkit surface instead of rebuilding a `PropertiesPanel`
float field.
float field. Trade target-cargo selection now lowers through a local
Eve/UI Toolkit surface instead of rebuilding a `ContextMenu` option list.
- The combat schematic HUD uses typed runtime catalog weapon facets for its
static weapon icon strip; missing typed weapon facets no longer fall back to
legacy `WeaponItemData`. Schematic weapon-row selection now uses typed
@ -1564,6 +1565,10 @@ First Aetheria surfaces to publish:
gameplay commit primitive still owns mutation, but the old
`PropertiesPanel.AddField("Shutdown Threshold", ...)` path no longer owns
that settings shell.
- Done: replace the trade target-cargo selector popup with an Eve-owned UI
Toolkit surface. `TradeMenu` still owns local target-cargo presentation
state, but the old `ContextMenu.AddOption(...)` popup path no longer owns
that selector shell.
- Done: move the shared `org.gamecult.eve.surface` and
`org.gamecult.eve.unity-uitoolkit` packages into the Eve repo and retarget
Aetheria to import them from there instead of carrying local staged
@ -1931,6 +1936,9 @@ First Aetheria surfaces to publish:
- Inventory background ship tuning now lowers through a shared UI Toolkit
`UIDocument` host and a local Eve surface document instead of rebuilding a
mutable `PropertiesPanel.AddField(...)` control for shutdown threshold.
- Trade target-cargo selection now lowers through a shared UI Toolkit
`UIDocument` host and a local Eve surface document instead of rebuilding a
`ContextMenu.AddOption(...)` popup for docking-bay and ship-bay choices.
- Keyboard display layout parsing is no longer JSON-backed:
`InputDisplayLayout` builds its static ANSI-104 display projection from typed
`InputLayout` rows/columns. The dead commented Ink `ToJson` write path and