581 lines
23 KiB
C#
581 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using GameCult.Eve.Surface;
|
|
|
|
namespace GameCult.Aetheria.State.Verse
|
|
{
|
|
public sealed class AetheriaRuntimeTradeSurfaceOption
|
|
{
|
|
public AetheriaRuntimeTradeSurfaceOption(string id, string label, string command)
|
|
{
|
|
Id = id ?? "";
|
|
Label = label ?? "";
|
|
Command = command ?? "";
|
|
}
|
|
|
|
public string Id { get; }
|
|
public string Label { get; }
|
|
public string Command { get; }
|
|
}
|
|
|
|
public sealed class AetheriaRuntimeTradeSurfaceGroup
|
|
{
|
|
public AetheriaRuntimeTradeSurfaceGroup(
|
|
string id,
|
|
string title,
|
|
IReadOnlyList<AetheriaRuntimeTradeSurfaceOption> options)
|
|
{
|
|
Id = id ?? "";
|
|
Title = title ?? "";
|
|
Options = options ?? Array.Empty<AetheriaRuntimeTradeSurfaceOption>();
|
|
}
|
|
|
|
public string Id { get; }
|
|
public string Title { get; }
|
|
public IReadOnlyList<AetheriaRuntimeTradeSurfaceOption> Options { get; }
|
|
}
|
|
|
|
public enum AetheriaRuntimeTradeFilterSelectionKind
|
|
{
|
|
Unknown = 0,
|
|
Hardpoint = 1,
|
|
SimpleCommodity = 2,
|
|
CompoundCommodity = 3,
|
|
Behavior = 4,
|
|
MinimumSize = 5,
|
|
MaximumSize = 6
|
|
}
|
|
|
|
public sealed class AetheriaRuntimeTradeFilterOption
|
|
{
|
|
public AetheriaRuntimeTradeFilterOption(
|
|
AetheriaRuntimeTradeFilterSelectionKind kind,
|
|
string token,
|
|
string label)
|
|
{
|
|
Kind = kind;
|
|
Token = token ?? "";
|
|
Label = label ?? "";
|
|
}
|
|
|
|
public AetheriaRuntimeTradeFilterSelectionKind Kind { get; }
|
|
public string Token { get; }
|
|
public string Label { get; }
|
|
}
|
|
|
|
public readonly struct AetheriaRuntimeTradeFilterSelection
|
|
{
|
|
public AetheriaRuntimeTradeFilterSelection(
|
|
AetheriaRuntimeTradeFilterSelectionKind kind,
|
|
string command,
|
|
string token)
|
|
{
|
|
Kind = kind;
|
|
Command = command ?? "";
|
|
Token = token ?? "";
|
|
}
|
|
|
|
public AetheriaRuntimeTradeFilterSelectionKind Kind { get; }
|
|
public string Command { get; }
|
|
public string Token { get; }
|
|
}
|
|
|
|
public sealed class AetheriaRuntimeTradeFilterSurfaceModel
|
|
{
|
|
public AetheriaRuntimeTradeFilterSurfaceModel(
|
|
AetheriaRuntimeSurfaceDocument document,
|
|
IReadOnlyDictionary<string, AetheriaRuntimeTradeFilterSelection> selections)
|
|
{
|
|
Document = document ?? throw new ArgumentNullException(nameof(document));
|
|
Selections = selections ?? new Dictionary<string, AetheriaRuntimeTradeFilterSelection>(StringComparer.Ordinal);
|
|
}
|
|
|
|
public AetheriaRuntimeSurfaceDocument Document { get; }
|
|
public IReadOnlyDictionary<string, AetheriaRuntimeTradeFilterSelection> Selections { get; }
|
|
|
|
public bool TryResolve(string command, out AetheriaRuntimeTradeFilterSelection selection)
|
|
{
|
|
return Selections.TryGetValue(command ?? "", out selection);
|
|
}
|
|
}
|
|
|
|
public sealed class AetheriaRuntimeTradeRowActionOption
|
|
{
|
|
public AetheriaRuntimeTradeRowActionOption(int index, string label)
|
|
{
|
|
Index = index;
|
|
Label = label ?? "";
|
|
}
|
|
|
|
public int Index { get; }
|
|
public string Label { get; }
|
|
}
|
|
|
|
public readonly struct AetheriaRuntimeTradeRowActionSelection
|
|
{
|
|
public AetheriaRuntimeTradeRowActionSelection(string command, int index)
|
|
{
|
|
Command = command ?? "";
|
|
Index = index;
|
|
}
|
|
|
|
public string Command { get; }
|
|
public int Index { get; }
|
|
}
|
|
|
|
public sealed class AetheriaRuntimeTradeRowActionSurfaceModel
|
|
{
|
|
public AetheriaRuntimeTradeRowActionSurfaceModel(
|
|
AetheriaRuntimeSurfaceDocument document,
|
|
IReadOnlyDictionary<string, AetheriaRuntimeTradeRowActionSelection> selections)
|
|
{
|
|
Document = document ?? throw new ArgumentNullException(nameof(document));
|
|
Selections = selections ?? new Dictionary<string, AetheriaRuntimeTradeRowActionSelection>(StringComparer.Ordinal);
|
|
}
|
|
|
|
public AetheriaRuntimeSurfaceDocument Document { get; }
|
|
public IReadOnlyDictionary<string, AetheriaRuntimeTradeRowActionSelection> Selections { get; }
|
|
|
|
public bool TryResolve(string command, out AetheriaRuntimeTradeRowActionSelection selection)
|
|
{
|
|
return Selections.TryGetValue(command ?? "", out selection);
|
|
}
|
|
}
|
|
|
|
public static class AetheriaRuntimeTradeInteractionSurfaceBuilder
|
|
{
|
|
public const string FilterSurfaceId = "aetheria.trade.filter_selector";
|
|
public const string CloseFilter = "aetheria.trade.filter_selector.close";
|
|
public const string RowActionSurfaceId = "aetheria.trade.row_actions";
|
|
public const string CloseRowAction = "aetheria.trade.row_actions.close";
|
|
|
|
public static string HardpointFilterCommand(string hardpointType)
|
|
{
|
|
return $"{FilterSurfaceId}.hardpoint.{hardpointType ?? ""}";
|
|
}
|
|
|
|
public static string SimpleCommodityFilterCommand(string commodityType)
|
|
{
|
|
return $"{FilterSurfaceId}.simple.{commodityType ?? ""}";
|
|
}
|
|
|
|
public static string CompoundCommodityFilterCommand(string commodityType)
|
|
{
|
|
return $"{FilterSurfaceId}.compound.{commodityType ?? ""}";
|
|
}
|
|
|
|
public static string BehaviorFilterCommand(string behaviorKind)
|
|
{
|
|
return $"{FilterSurfaceId}.behavior.{behaviorKind ?? ""}";
|
|
}
|
|
|
|
public static string MinimumSizeFilterCommand()
|
|
{
|
|
return $"{FilterSurfaceId}.size.minimum";
|
|
}
|
|
|
|
public static string MaximumSizeFilterCommand()
|
|
{
|
|
return $"{FilterSurfaceId}.size.maximum";
|
|
}
|
|
|
|
public static string RowActionCommand(int index)
|
|
{
|
|
return $"{RowActionSurfaceId}.action_{index}";
|
|
}
|
|
|
|
public static AetheriaRuntimeTradeFilterSurfaceModel BuildFilters(
|
|
string filterSummary,
|
|
IEnumerable<AetheriaRuntimeTradeFilterOption> options,
|
|
string updatedAtUtc)
|
|
{
|
|
var selections = new Dictionary<string, AetheriaRuntimeTradeFilterSelection>(StringComparer.Ordinal);
|
|
var grouped = new Dictionary<string, (AetheriaRuntimeTradeFilterSelectionKind Kind, List<AetheriaRuntimeTradeSurfaceOption> Options)>(StringComparer.Ordinal);
|
|
|
|
foreach (var option in options ?? Array.Empty<AetheriaRuntimeTradeFilterOption>())
|
|
{
|
|
if (option == null || option.Kind == AetheriaRuntimeTradeFilterSelectionKind.Unknown)
|
|
continue;
|
|
|
|
var command = FilterCommand(option.Kind, option.Token);
|
|
if (string.IsNullOrWhiteSpace(command))
|
|
continue;
|
|
|
|
var groupKey = FilterGroupKey(option.Kind);
|
|
if (!grouped.TryGetValue(groupKey, out var group))
|
|
{
|
|
group = (option.Kind, new List<AetheriaRuntimeTradeSurfaceOption>());
|
|
grouped[groupKey] = group;
|
|
}
|
|
|
|
group.Options.Add(new AetheriaRuntimeTradeSurfaceOption(
|
|
$"{FilterSurfaceId}.{groupKey}.{StableToken(option.Token)}",
|
|
string.IsNullOrWhiteSpace(option.Label) ? option.Token : option.Label,
|
|
command));
|
|
selections[command] = new AetheriaRuntimeTradeFilterSelection(
|
|
option.Kind,
|
|
command,
|
|
option.Token);
|
|
}
|
|
|
|
var groups = grouped
|
|
.OrderBy(entry => FilterGroupOrder(entry.Value.Kind))
|
|
.Select(entry => new AetheriaRuntimeTradeSurfaceGroup(
|
|
$"{FilterSurfaceId}.{entry.Key}.card",
|
|
FilterGroupTitle(entry.Value.Kind),
|
|
entry.Value.Options
|
|
.OrderBy(option => option.Label, StringComparer.Ordinal)
|
|
.ToArray()))
|
|
.Where(group => group.Options.Count > 0)
|
|
.ToArray();
|
|
|
|
return new AetheriaRuntimeTradeFilterSurfaceModel(
|
|
BuildFilter(filterSummary, groups, updatedAtUtc),
|
|
selections);
|
|
}
|
|
|
|
public static AetheriaRuntimeTradeRowActionSurfaceModel BuildRowActions(
|
|
string title,
|
|
IEnumerable<AetheriaRuntimeTradeRowActionOption> actions,
|
|
string updatedAtUtc)
|
|
{
|
|
var options = new List<AetheriaRuntimeTradeSurfaceOption>();
|
|
var selections = new Dictionary<string, AetheriaRuntimeTradeRowActionSelection>(StringComparer.Ordinal);
|
|
|
|
foreach (var action in actions ?? Array.Empty<AetheriaRuntimeTradeRowActionOption>())
|
|
{
|
|
if (action == null || action.Index < 0)
|
|
continue;
|
|
|
|
var command = RowActionCommand(action.Index);
|
|
var label = string.IsNullOrWhiteSpace(action.Label)
|
|
? $"Action {action.Index + 1}"
|
|
: action.Label;
|
|
options.Add(new AetheriaRuntimeTradeSurfaceOption(
|
|
$"{RowActionSurfaceId}.action_{action.Index}",
|
|
label,
|
|
command));
|
|
selections[command] = new AetheriaRuntimeTradeRowActionSelection(command, action.Index);
|
|
}
|
|
|
|
return new AetheriaRuntimeTradeRowActionSurfaceModel(
|
|
BuildRowActions(title, options, updatedAtUtc),
|
|
selections);
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceDocument BuildFilter(
|
|
string filterSummary,
|
|
IReadOnlyList<AetheriaRuntimeTradeSurfaceGroup> groups,
|
|
string updatedAtUtc,
|
|
long version = 1)
|
|
{
|
|
groups ??= Array.Empty<AetheriaRuntimeTradeSurfaceGroup>();
|
|
|
|
var children = new List<AetheriaRuntimeSurfaceComponent>
|
|
{
|
|
Card(
|
|
$"{FilterSurfaceId}.summary",
|
|
"Trade Filters",
|
|
Text(
|
|
$"{FilterSurfaceId}.note",
|
|
"The observing client lists available trade filters; the shared runtime surface owns the filter selector contract."),
|
|
Text($"{FilterSurfaceId}.active", filterSummary))
|
|
};
|
|
|
|
foreach (var group in groups)
|
|
{
|
|
if (group?.Options == null || group.Options.Count == 0)
|
|
continue;
|
|
|
|
children.Add(Card(
|
|
group.Id,
|
|
group.Title,
|
|
ButtonColumn(
|
|
$"{group.Id}.options",
|
|
group.Options
|
|
.Select(option => Button(option.Id, option.Label, option.Command))
|
|
.ToArray())));
|
|
}
|
|
|
|
children.Add(ButtonRow(
|
|
$"{FilterSurfaceId}.actions",
|
|
Button($"{FilterSurfaceId}.close", "Close", CloseFilter)));
|
|
|
|
return new AetheriaRuntimeSurfaceDocument(
|
|
providerId: "aetheria",
|
|
providerKind: "trade.menu",
|
|
title: "Trade Filter Selector",
|
|
version: version,
|
|
updatedAtUtc: updatedAtUtc ?? "",
|
|
surface: new AetheriaRuntimeSurfaceTree(
|
|
FilterSurfaceId,
|
|
Node(
|
|
$"{FilterSurfaceId}.root",
|
|
"surface",
|
|
Array.Empty<(string Key, string Value)>(),
|
|
children.ToArray()),
|
|
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
|
|
commands: groups
|
|
.Where(group => group?.Options != null)
|
|
.SelectMany(group => group.Options)
|
|
.Select(option => new AetheriaRuntimeSurfaceCommandTemplate(option.Command, option.Label, AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport))
|
|
.Append(new AetheriaRuntimeSurfaceCommandTemplate(CloseFilter, "Close", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport))
|
|
.ToArray());
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceDocument BuildRowActions(
|
|
string title,
|
|
IReadOnlyList<AetheriaRuntimeTradeSurfaceOption> actions,
|
|
string updatedAtUtc,
|
|
long version = 1)
|
|
{
|
|
actions ??= Array.Empty<AetheriaRuntimeTradeSurfaceOption>();
|
|
|
|
return new AetheriaRuntimeSurfaceDocument(
|
|
providerId: "aetheria",
|
|
providerKind: "trade.menu",
|
|
title: "Trade Row Actions",
|
|
version: version,
|
|
updatedAtUtc: updatedAtUtc ?? "",
|
|
surface: new AetheriaRuntimeSurfaceTree(
|
|
RowActionSurfaceId,
|
|
Node(
|
|
$"{RowActionSurfaceId}.root",
|
|
"surface",
|
|
Array.Empty<(string Key, string Value)>(),
|
|
Card(
|
|
$"{RowActionSurfaceId}.card",
|
|
"Trade Action",
|
|
Text($"{RowActionSurfaceId}.title", title),
|
|
Text(
|
|
$"{RowActionSurfaceId}.note",
|
|
"The observing client lists available row actions; the shared runtime surface owns the row action contract."),
|
|
ButtonColumn(
|
|
$"{RowActionSurfaceId}.options",
|
|
actions
|
|
.Select(action => Button(action.Id, action.Label, action.Command))
|
|
.ToArray()),
|
|
ButtonRow(
|
|
$"{RowActionSurfaceId}.actions",
|
|
Button($"{RowActionSurfaceId}.close", "Close", CloseRowAction)))),
|
|
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
|
|
commands: actions
|
|
.Select(action => new AetheriaRuntimeSurfaceCommandTemplate(action.Command, action.Label, AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport))
|
|
.Append(new AetheriaRuntimeSurfaceCommandTemplate(CloseRowAction, "Close", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport))
|
|
.ToArray());
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceComponent Card(
|
|
string id,
|
|
string title,
|
|
params AetheriaRuntimeSurfaceComponent[] children)
|
|
{
|
|
return Node(id, "card", new[] { ("title", title ?? "") }, children);
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceComponent Text(string id, string value)
|
|
{
|
|
return Node(id, "text", new[] { ("value", value ?? "") });
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceComponent Button(string id, string label, string command)
|
|
{
|
|
return Node(id, "control.button", new[] { ("label", label ?? ""), ("command", command ?? "") });
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceComponent ButtonRow(
|
|
string id,
|
|
params AetheriaRuntimeSurfaceComponent[] children)
|
|
{
|
|
return Node(id, "row", Array.Empty<(string Key, string Value)>(), children);
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceComponent ButtonColumn(
|
|
string id,
|
|
params AetheriaRuntimeSurfaceComponent[] children)
|
|
{
|
|
return Node(id, "column", Array.Empty<(string Key, string Value)>(), children);
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceComponent Node(
|
|
string id,
|
|
string kind,
|
|
IEnumerable<(string Key, string Value)> props,
|
|
params AetheriaRuntimeSurfaceComponent[] children)
|
|
{
|
|
return new AetheriaRuntimeSurfaceComponent(
|
|
id ?? "",
|
|
kind ?? "",
|
|
(props ?? Array.Empty<(string Key, string Value)>())
|
|
.ToDictionary(prop => prop.Key, prop => prop.Value ?? "", StringComparer.Ordinal),
|
|
children ?? Array.Empty<AetheriaRuntimeSurfaceComponent>());
|
|
}
|
|
|
|
private static string FilterCommand(AetheriaRuntimeTradeFilterSelectionKind kind, string token)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case AetheriaRuntimeTradeFilterSelectionKind.Hardpoint:
|
|
return HardpointFilterCommand(token);
|
|
case AetheriaRuntimeTradeFilterSelectionKind.SimpleCommodity:
|
|
return SimpleCommodityFilterCommand(token);
|
|
case AetheriaRuntimeTradeFilterSelectionKind.CompoundCommodity:
|
|
return CompoundCommodityFilterCommand(token);
|
|
case AetheriaRuntimeTradeFilterSelectionKind.Behavior:
|
|
return BehaviorFilterCommand(token);
|
|
case AetheriaRuntimeTradeFilterSelectionKind.MinimumSize:
|
|
return MinimumSizeFilterCommand();
|
|
case AetheriaRuntimeTradeFilterSelectionKind.MaximumSize:
|
|
return MaximumSizeFilterCommand();
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
private static string FilterGroupKey(AetheriaRuntimeTradeFilterSelectionKind kind)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case AetheriaRuntimeTradeFilterSelectionKind.Hardpoint:
|
|
return "hardpoint";
|
|
case AetheriaRuntimeTradeFilterSelectionKind.SimpleCommodity:
|
|
return "simple";
|
|
case AetheriaRuntimeTradeFilterSelectionKind.CompoundCommodity:
|
|
return "compound";
|
|
case AetheriaRuntimeTradeFilterSelectionKind.Behavior:
|
|
return "behavior";
|
|
case AetheriaRuntimeTradeFilterSelectionKind.MinimumSize:
|
|
case AetheriaRuntimeTradeFilterSelectionKind.MaximumSize:
|
|
return "size";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
private static string FilterGroupTitle(AetheriaRuntimeTradeFilterSelectionKind kind)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case AetheriaRuntimeTradeFilterSelectionKind.Hardpoint:
|
|
return "Gear Type";
|
|
case AetheriaRuntimeTradeFilterSelectionKind.SimpleCommodity:
|
|
return "Simple Commodity";
|
|
case AetheriaRuntimeTradeFilterSelectionKind.CompoundCommodity:
|
|
return "Compound Commodity";
|
|
case AetheriaRuntimeTradeFilterSelectionKind.Behavior:
|
|
return "Item Behavior";
|
|
case AetheriaRuntimeTradeFilterSelectionKind.MinimumSize:
|
|
case AetheriaRuntimeTradeFilterSelectionKind.MaximumSize:
|
|
return "Size";
|
|
default:
|
|
return "Filters";
|
|
}
|
|
}
|
|
|
|
private static int FilterGroupOrder(AetheriaRuntimeTradeFilterSelectionKind kind)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case AetheriaRuntimeTradeFilterSelectionKind.Hardpoint:
|
|
return 0;
|
|
case AetheriaRuntimeTradeFilterSelectionKind.SimpleCommodity:
|
|
return 1;
|
|
case AetheriaRuntimeTradeFilterSelectionKind.CompoundCommodity:
|
|
return 2;
|
|
case AetheriaRuntimeTradeFilterSelectionKind.Behavior:
|
|
return 3;
|
|
case AetheriaRuntimeTradeFilterSelectionKind.MinimumSize:
|
|
case AetheriaRuntimeTradeFilterSelectionKind.MaximumSize:
|
|
return 4;
|
|
default:
|
|
return 99;
|
|
}
|
|
}
|
|
|
|
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 AetheriaRuntimeTradeInteractionCommandKind
|
|
{
|
|
Unknown = 0,
|
|
Close = 1,
|
|
Select = 2
|
|
}
|
|
|
|
public readonly struct AetheriaRuntimeTradeInteractionCommand
|
|
{
|
|
public AetheriaRuntimeTradeInteractionCommand(
|
|
AetheriaRuntimeTradeInteractionCommandKind kind,
|
|
string command)
|
|
{
|
|
Kind = kind;
|
|
Command = command ?? "";
|
|
}
|
|
|
|
public AetheriaRuntimeTradeInteractionCommandKind Kind { get; }
|
|
public string Command { get; }
|
|
}
|
|
|
|
public static class AetheriaRuntimeTradeInteractionSurfaceCommands
|
|
{
|
|
public static bool TryReadFilter(
|
|
EveSurfaceCommandRequest request,
|
|
out AetheriaRuntimeTradeInteractionCommand command)
|
|
{
|
|
return TryRead(
|
|
request,
|
|
AetheriaRuntimeTradeInteractionSurfaceBuilder.FilterSurfaceId,
|
|
AetheriaRuntimeTradeInteractionSurfaceBuilder.CloseFilter,
|
|
out command);
|
|
}
|
|
|
|
public static bool TryReadRowAction(
|
|
EveSurfaceCommandRequest request,
|
|
out AetheriaRuntimeTradeInteractionCommand command)
|
|
{
|
|
return TryRead(
|
|
request,
|
|
AetheriaRuntimeTradeInteractionSurfaceBuilder.RowActionSurfaceId,
|
|
AetheriaRuntimeTradeInteractionSurfaceBuilder.CloseRowAction,
|
|
out command);
|
|
}
|
|
|
|
private static bool TryRead(
|
|
EveSurfaceCommandRequest request,
|
|
string surfaceId,
|
|
string closeCommand,
|
|
out AetheriaRuntimeTradeInteractionCommand command)
|
|
{
|
|
command = default;
|
|
if (request == null ||
|
|
!string.Equals(request.SurfaceId, surfaceId, StringComparison.Ordinal))
|
|
return false;
|
|
|
|
var commandText = request.Operation?.OperationId ?? "";
|
|
if (string.Equals(commandText, closeCommand, StringComparison.Ordinal))
|
|
{
|
|
command = new AetheriaRuntimeTradeInteractionCommand(
|
|
AetheriaRuntimeTradeInteractionCommandKind.Close,
|
|
commandText);
|
|
return true;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(commandText))
|
|
return false;
|
|
|
|
command = new AetheriaRuntimeTradeInteractionCommand(
|
|
AetheriaRuntimeTradeInteractionCommandKind.Select,
|
|
commandText);
|
|
return true;
|
|
}
|
|
}
|
|
}
|