diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 12202eb8c..59bbcb594 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -4536,14 +4536,18 @@ static void RequireTradeCargoSelectorUseEveSurface(string root) var requiredSymbols = new[] { "RenderCargoSelectorSurface(", - "BuildCargoSelectionCommands(", + "_cargoSelectorSurfaceProjection = ProjectTradeCargoSelectorSurface();", "HandleCargoSelectorSurfaceCommand(", "AetheriaEveUnitySurfaceHost.RenderRuntime(", "AetheriaEveUnitySurfaceHost.Hide(_cargoSelectorSurfaceDocument)", - "AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.Build(ProjectTradeCargoSelectorSurfaceState(", - "ProjectTradeCargoSelectorSurfaceState(", - "AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.DockingBay", - "AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.ShipBayCommand(", + "AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.Build(_cargoSelectorSurfaceProjection.State)", + "ProjectTradeCargoSelectorSurface(", + "AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.Project(", + "_cargoSelectorSurfaceProjection?.TryResolve(command.Command, out var selection) == true", + "ApplyCargoSelection(", + "new AetheriaRuntimeTradeCargoProjectionOption(", + "AetheriaRuntimeTradeCargoTargetKind.DockingBay", + "AetheriaRuntimeTradeCargoTargetKind.ShipBay", "AetheriaRuntimeTradeCargoSelectorSurfaceCommands.TryRead(request, out var command)", "AetheriaRuntimeTradeCargoSelectorCommandKind.Close", "AetheriaRuntimeTradeCargoSelectorCommandKind.Select", @@ -4578,6 +4582,9 @@ static void RequireTradeCargoSelectorUseEveSurface(string root) "new EveSurfaceComponent(", "string.Equals(request.Command, AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.Close", "_cargoSelectionCommands.TryGetValue(request.Command", + "private readonly Dictionary _cargoSelectionCommands", + "BuildCargoSelectionCommands(", + "ProjectTradeCargoSelectorSurfaceState(", "GameManager.DockedEntity.Children", "GameManager.CurrentEntity.Parent.Children", "GameManager.DockedEntity == null", @@ -4608,7 +4615,13 @@ static void RequireTradeCargoSelectorUseEveSurface(string root) "public static bool TryRead(", "AetheriaRuntimeTradeCargoSelectorSurfaceState", "AetheriaRuntimeTradeCargoTargetOption", + "AetheriaRuntimeTradeCargoProjectionOption", + "AetheriaRuntimeTradeCargoTargetKind", + "AetheriaRuntimeTradeCargoSelection", + "AetheriaRuntimeTradeCargoSelectorSurfaceProjection", "public static string ShipBayCommand(", + "public static AetheriaRuntimeTradeCargoSelectorSurfaceProjection Project(", + "public bool TryResolve(string command, out AetheriaRuntimeTradeCargoSelection selection)", "public static AetheriaRuntimeSurfaceDocument Build(", "providerKind: \"trade.menu\"", "The observing client projects available cargo targets; the shared runtime surface owns the cargo selector contract." diff --git a/Assets/Scripts/UI/Menu/TradeMenu.cs b/Assets/Scripts/UI/Menu/TradeMenu.cs index 89d9d0d67..e657ed13f 100644 --- a/Assets/Scripts/UI/Menu/TradeMenu.cs +++ b/Assets/Scripts/UI/Menu/TradeMenu.cs @@ -29,8 +29,6 @@ public class TradeMenu : MonoBehaviour private (ItemFilter filter, SimpleCommodityCategory type) _commodityFilter; private (ItemFilter filter, CompoundCommodityCategory type) _compoundCommodityFilter; private List _behaviorFilters = new List(); - private readonly Dictionary _cargoSelectionCommands = - new Dictionary(StringComparer.Ordinal); private UIDocument _cargoSelectorSurfaceDocument; private UIDocument _filterSurfaceDocument; private UIDocument _rowActionSurfaceDocument; @@ -39,6 +37,7 @@ public class TradeMenu : MonoBehaviour private readonly AetheriaEveUnitySurfaceChrome _filterSurfaceChrome = PanelChrome(420f, 520f, Align.FlexStart); private readonly AetheriaEveUnitySurfaceChrome _rowActionSurfaceChrome = PanelChrome(320f, 360f, Align.FlexStart); private readonly AetheriaEveUnitySurfaceChrome _tradeItemSurfaceChrome = PanelChrome(420f, 520f, Align.FlexStart); + private AetheriaRuntimeTradeCargoSelectorSurfaceProjection _cargoSelectorSurfaceProjection; private AetheriaRuntimeTradeFilterSurfaceProjection _filterSurfaceProjection; private Action[] _rowActionCallbacks = Array.Empty(); private AetheriaRuntimeTradeRowActionSurfaceProjection _rowActionSurfaceProjection; @@ -1022,46 +1021,17 @@ public class TradeMenu : MonoBehaviour private void RenderCargoSelectorSurface() { - BuildCargoSelectionCommands(); + _cargoSelectorSurfaceProjection = ProjectTradeCargoSelectorSurface(); _cargoSelectorSurfaceDocument = AetheriaEveUnitySurfaceHost.RenderRuntime( transform, _cargoSelectorSurfaceDocument, "Aetheria Trade Cargo Selector Surface", - AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.Build(ProjectTradeCargoSelectorSurfaceState()), + AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.Build(_cargoSelectorSurfaceProjection.State), HandleCargoSelectorSurfaceCommand, _cargoSelectorSurfaceChrome); } - private void BuildCargoSelectionCommands() - { - _cargoSelectionCommands.Clear(); - - if (!GameManager.TryGetObservedDockingBay(out var dockingBay)) - return; - - if (_targetCargo != dockingBay) - { - _cargoSelectionCommands[AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.DockingBay] = - (dockingBay, "Docking Bay"); - } - - foreach (var ship in GameManager.ObservedAvailableEntities() - .OfType() - .Where(ship => ship.IsPlayerShip) - .Select((ship, shipIndex) => (ship, shipIndex))) - { - foreach (var bay in ship.ship.CargoBays.Select((cargoBay, index) => (cargoBay, index))) - { - if (_targetCargo == bay.cargoBay) - continue; - - var command = AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.ShipBayCommand(ship.shipIndex, bay.index); - _cargoSelectionCommands[command] = (bay.cargoBay, $"{ship.ship.Name} Bay {bay.index + 1}"); - } - } - } - private void HandleCargoSelectorSurfaceCommand(EveSurfaceCommandRequest request) { if (!AetheriaRuntimeTradeCargoSelectorSurfaceCommands.TryRead(request, out var command)) @@ -1077,10 +1047,9 @@ public class TradeMenu : MonoBehaviour } if (command.Kind == AetheriaRuntimeTradeCargoSelectorCommandKind.Select && - _cargoSelectionCommands.TryGetValue(command.Command, out var option)) + _cargoSelectorSurfaceProjection?.TryResolve(command.Command, out var selection) == true) { - _targetCargo = option.Cargo; - TargetCargoLabel.text = option.Label; + ApplyCargoSelection(selection); HideCargoSelectorSurface(); Populate(); return; @@ -1116,22 +1085,62 @@ public class TradeMenu : MonoBehaviour }; } - private AetheriaRuntimeTradeCargoSelectorSurfaceState ProjectTradeCargoSelectorSurfaceState() + private AetheriaRuntimeTradeCargoSelectorSurfaceProjection ProjectTradeCargoSelectorSurface() { - var targets = _cargoSelectionCommands - .OrderBy(pair => pair.Value.Label, StringComparer.Ordinal) - .Select(pair => new AetheriaRuntimeTradeCargoTargetOption( - $"{AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.SurfaceId}.{pair.Key.Split('.').Last()}", - pair.Value.Label, - pair.Key)) - .ToArray(); + var targets = new List(); + if (GameManager.TryGetObservedDockingBay(out var dockingBay)) + { + targets.Add(new AetheriaRuntimeTradeCargoProjectionOption( + AetheriaRuntimeTradeCargoTargetKind.DockingBay, + "Docking Bay", + isCurrent: _targetCargo == dockingBay)); + } - return new AetheriaRuntimeTradeCargoSelectorSurfaceState( + targets.AddRange(GameManager.ObservedAvailableEntities() + .OfType() + .Where(ship => ship.IsPlayerShip) + .SelectMany((ship, shipIndex) => ship.CargoBays.Select((cargoBay, bayIndex) => + new AetheriaRuntimeTradeCargoProjectionOption( + AetheriaRuntimeTradeCargoTargetKind.ShipBay, + $"{ship.Name} Bay {bayIndex + 1}", + shipIndex, + bayIndex, + _targetCargo == cargoBay)))); + + return AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.Project( TargetCargoLabel.text ?? "", targets, DateTime.UtcNow.ToString("O")); } + private void ApplyCargoSelection(AetheriaRuntimeTradeCargoSelection selection) + { + switch (selection.Kind) + { + case AetheriaRuntimeTradeCargoTargetKind.DockingBay: + if (GameManager.TryGetObservedDockingBay(out var dockingBay)) + { + _targetCargo = dockingBay; + TargetCargoLabel.text = selection.Label; + } + return; + case AetheriaRuntimeTradeCargoTargetKind.ShipBay: + var ships = GameManager.ObservedAvailableEntities() + .OfType() + .Where(ship => ship.IsPlayerShip) + .ToArray(); + if (selection.ShipIndex >= 0 && + selection.ShipIndex < ships.Length && + selection.BayIndex >= 0 && + selection.BayIndex < ships[selection.ShipIndex].CargoBays.Count) + { + _targetCargo = ships[selection.ShipIndex].CargoBays[selection.BayIndex]; + TargetCargoLabel.text = selection.Label; + } + return; + } + } + private int CountAvailablePlayerShips(string itemKey) { return GameManager.ObservedAvailableEntities() diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.cs index f80260412..652f6e18b 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeTradeCargoSelectorSurfaceBuilder.cs @@ -36,6 +36,81 @@ namespace GameCult.Aetheria.State.Verse public string UpdatedAtUtc { get; } } + public enum AetheriaRuntimeTradeCargoTargetKind + { + Unknown = 0, + DockingBay = 1, + ShipBay = 2 + } + + public sealed class AetheriaRuntimeTradeCargoProjectionOption + { + public AetheriaRuntimeTradeCargoProjectionOption( + AetheriaRuntimeTradeCargoTargetKind kind, + string label, + int shipIndex = -1, + int bayIndex = -1, + bool isCurrent = false) + { + Kind = kind; + Label = label ?? ""; + ShipIndex = shipIndex; + BayIndex = bayIndex; + IsCurrent = isCurrent; + } + + public AetheriaRuntimeTradeCargoTargetKind Kind { get; } + public string Label { get; } + public int ShipIndex { get; } + public int BayIndex { get; } + public bool IsCurrent { get; } + } + + public readonly struct AetheriaRuntimeTradeCargoSelection + { + public AetheriaRuntimeTradeCargoSelection( + AetheriaRuntimeTradeCargoTargetKind kind, + string command, + string label, + int shipIndex = -1, + int bayIndex = -1) + { + Kind = kind; + Command = command ?? ""; + Label = label ?? ""; + ShipIndex = shipIndex; + BayIndex = bayIndex; + } + + public AetheriaRuntimeTradeCargoTargetKind Kind { get; } + public string Command { get; } + public string Label { get; } + public int ShipIndex { get; } + public int BayIndex { get; } + } + + public sealed class AetheriaRuntimeTradeCargoSelectorSurfaceProjection + { + public AetheriaRuntimeTradeCargoSelectorSurfaceProjection( + AetheriaRuntimeTradeCargoSelectorSurfaceState state, + IReadOnlyDictionary selections) + { + State = state ?? new AetheriaRuntimeTradeCargoSelectorSurfaceState( + "", + Array.Empty(), + ""); + Selections = selections ?? new Dictionary(StringComparer.Ordinal); + } + + public AetheriaRuntimeTradeCargoSelectorSurfaceState State { get; } + public IReadOnlyDictionary Selections { get; } + + public bool TryResolve(string command, out AetheriaRuntimeTradeCargoSelection selection) + { + return Selections.TryGetValue(command ?? "", out selection); + } + } + public static class AetheriaRuntimeTradeCargoSelectorSurfaceBuilder { public const string SurfaceId = "aetheria.trade.target_cargo_selector"; @@ -47,6 +122,50 @@ namespace GameCult.Aetheria.State.Verse return $"{SurfaceId}.ship_{shipIndex}_bay_{bayIndex}"; } + public static AetheriaRuntimeTradeCargoSelectorSurfaceProjection Project( + string currentTarget, + IEnumerable targets, + string updatedAtUtc) + { + var options = new List(); + var selections = new Dictionary(StringComparer.Ordinal); + + foreach (var target in targets ?? Array.Empty()) + { + if (target == null || + target.IsCurrent || + target.Kind == AetheriaRuntimeTradeCargoTargetKind.Unknown) + { + continue; + } + + var command = CommandFor(target); + if (string.IsNullOrWhiteSpace(command)) + continue; + + var label = string.IsNullOrWhiteSpace(target.Label) + ? command + : target.Label; + options.Add(new AetheriaRuntimeTradeCargoTargetOption( + $"{SurfaceId}.{StableToken(command)}", + label, + command)); + selections[command] = new AetheriaRuntimeTradeCargoSelection( + target.Kind, + command, + label, + target.ShipIndex, + target.BayIndex); + } + + return new AetheriaRuntimeTradeCargoSelectorSurfaceProjection( + new AetheriaRuntimeTradeCargoSelectorSurfaceState( + currentTarget, + options, + updatedAtUtc), + selections); + } + public static AetheriaRuntimeSurfaceDocument Build( AetheriaRuntimeTradeCargoSelectorSurfaceState state, long version = 1) @@ -144,6 +263,31 @@ namespace GameCult.Aetheria.State.Verse .ToDictionary(prop => prop.Key, prop => prop.Value ?? "", StringComparer.Ordinal), children ?? Array.Empty()); } + + private static string CommandFor(AetheriaRuntimeTradeCargoProjectionOption target) + { + switch (target.Kind) + { + case AetheriaRuntimeTradeCargoTargetKind.DockingBay: + return DockingBay; + case AetheriaRuntimeTradeCargoTargetKind.ShipBay: + return target.ShipIndex >= 0 && target.BayIndex >= 0 + ? ShipBayCommand(target.ShipIndex, target.BayIndex) + : ""; + default: + return ""; + } + } + + private static string StableToken(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return "empty"; + + return new string(value + .Select(character => char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : '-') + .ToArray()).Trim('-'); + } } public enum AetheriaRuntimeTradeCargoSelectorCommandKind