Aetheria/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeInputSettingsSurfaceBuilder.cs
2026-06-29 08:50:16 +01:00

538 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using GameCult.Eve.Surface;
namespace GameCult.Aetheria.State.Verse
{
public sealed class AetheriaRuntimeInputPathSurfaceLabel
{
public AetheriaRuntimeInputPathSurfaceLabel(string inputPath, string label)
{
InputPath = inputPath ?? "";
Label = label ?? "";
}
public string InputPath { get; }
public string Label { get; }
}
public sealed class AetheriaRuntimeObservedInputBinding
{
public AetheriaRuntimeObservedInputBinding(
string actionName,
int bindingIndex,
string bindingLabel,
string inputPath,
string inputLabel,
bool include)
{
ActionName = actionName ?? "";
BindingIndex = bindingIndex;
BindingLabel = bindingLabel ?? "";
InputPath = inputPath ?? "";
InputLabel = inputLabel ?? "";
Include = include;
}
public string ActionName { get; }
public int BindingIndex { get; }
public string BindingLabel { get; }
public string InputPath { get; }
public string InputLabel { get; }
public bool Include { get; }
}
public static class AetheriaRuntimeInputSettingsSurfaceBuilder
{
public static readonly IReadOnlyList<string> DefaultActionBarCandidatePaths = new[]
{
"<Mouse>/leftButton",
"<Mouse>/rightButton",
"<Mouse>/middleButton",
"<Mouse>/forwardButton",
"<Mouse>/backButton",
"<Keyboard>/1",
"<Keyboard>/2",
"<Keyboard>/3",
"<Keyboard>/4",
"<Keyboard>/5",
"<Keyboard>/leftShift"
};
public static readonly IReadOnlyList<AetheriaRuntimeInputPathSurfaceLabel> DefaultActionBarCandidateInputPaths =
new[]
{
new AetheriaRuntimeInputPathSurfaceLabel("<Mouse>/leftButton", "Left Button"),
new AetheriaRuntimeInputPathSurfaceLabel("<Mouse>/rightButton", "Right Button"),
new AetheriaRuntimeInputPathSurfaceLabel("<Mouse>/middleButton", "Middle Button"),
new AetheriaRuntimeInputPathSurfaceLabel("<Mouse>/forwardButton", "Forward Button"),
new AetheriaRuntimeInputPathSurfaceLabel("<Mouse>/backButton", "Back Button"),
new AetheriaRuntimeInputPathSurfaceLabel("<Keyboard>/1", "1"),
new AetheriaRuntimeInputPathSurfaceLabel("<Keyboard>/2", "2"),
new AetheriaRuntimeInputPathSurfaceLabel("<Keyboard>/3", "3"),
new AetheriaRuntimeInputPathSurfaceLabel("<Keyboard>/4", "4"),
new AetheriaRuntimeInputPathSurfaceLabel("<Keyboard>/5", "5"),
new AetheriaRuntimeInputPathSurfaceLabel("<Keyboard>/leftShift", "Left Shift")
};
public static bool IsSupportedCapturePath(string path)
{
return !string.IsNullOrWhiteSpace(path) &&
(path.StartsWith("<Keyboard>/", StringComparison.Ordinal) ||
path.StartsWith("<Mouse>/", StringComparison.Ordinal));
}
public static AetheriaRuntimeSurfaceDocument Build(
IEnumerable<AetheriaRuntimeObservedInputBinding> observedBindings,
IEnumerable<string> enabledActionBarInputPaths,
bool capturePending,
string capturePrompt,
string updatedAtUtc,
long version = 1)
{
var observed = (observedBindings ?? Array.Empty<AetheriaRuntimeObservedInputBinding>())
.Where(binding => binding != null)
.ToArray();
var actionBarCandidates = ComposeActionBarCandidates(observed, enabledActionBarInputPaths);
var bindings = ComposeBindingInputs(observed);
var actionBarInputs = ComposeActionBarInputs(enabledActionBarInputPaths, actionBarCandidates);
return Build(
bindings,
actionBarInputs,
capturePending,
capturePrompt,
updatedAtUtc,
version);
}
private static IReadOnlyList<InputBindingRow> ComposeBindingInputs(
IEnumerable<AetheriaRuntimeObservedInputBinding> observedBindings)
{
return (observedBindings ?? Array.Empty<AetheriaRuntimeObservedInputBinding>())
.Where(binding => binding != null &&
binding.Include &&
!string.IsNullOrWhiteSpace(binding.InputPath) &&
IsSupportedCapturePath(binding.InputPath))
.Select(binding => new InputBindingRow(
binding.ActionName,
binding.BindingIndex,
binding.BindingLabel,
string.IsNullOrWhiteSpace(binding.InputLabel) ? binding.InputPath : binding.InputLabel))
.OrderBy(binding => binding.BindingLabel, StringComparer.Ordinal)
.ThenBy(binding => binding.ActionName, StringComparer.Ordinal)
.ThenBy(binding => binding.BindingIndex)
.ToArray();
}
private static IReadOnlyList<AetheriaRuntimeInputPathSurfaceLabel> ComposeActionBarCandidates(
IEnumerable<AetheriaRuntimeObservedInputBinding> observedBindings,
IEnumerable<string> enabledInputPaths)
{
var candidates = new List<AetheriaRuntimeInputPathSurfaceLabel>();
candidates.AddRange(DefaultActionBarCandidateInputPaths);
foreach (var inputPath in enabledInputPaths ?? Array.Empty<string>())
{
if (!string.IsNullOrWhiteSpace(inputPath))
{
candidates.Add(new AetheriaRuntimeInputPathSurfaceLabel(
inputPath,
ReadInputLabel(inputPath, observedBindings)));
}
}
foreach (var binding in observedBindings ?? Array.Empty<AetheriaRuntimeObservedInputBinding>())
{
if (binding == null ||
!binding.Include ||
string.IsNullOrWhiteSpace(binding.InputPath) ||
!IsSupportedCapturePath(binding.InputPath))
{
continue;
}
candidates.Add(new AetheriaRuntimeInputPathSurfaceLabel(
binding.InputPath,
string.IsNullOrWhiteSpace(binding.InputLabel) ? binding.InputPath : binding.InputLabel));
}
return candidates;
}
private static IReadOnlyList<ActionBarInputRow> ComposeActionBarInputs(
IEnumerable<string> enabledInputPaths,
IEnumerable<AetheriaRuntimeInputPathSurfaceLabel> candidateInputPaths)
{
var enabledPaths = new HashSet<string>(
(enabledInputPaths ?? Array.Empty<string>()).Where(path => !string.IsNullOrWhiteSpace(path)),
StringComparer.Ordinal);
var candidates = new SortedDictionary<string, string>(StringComparer.Ordinal);
foreach (var candidate in candidateInputPaths ?? Array.Empty<AetheriaRuntimeInputPathSurfaceLabel>())
{
if (candidate == null || string.IsNullOrWhiteSpace(candidate.InputPath))
{
continue;
}
candidates[candidate.InputPath] = string.IsNullOrWhiteSpace(candidate.Label)
? candidate.InputPath
: candidate.Label;
}
foreach (var enabledPath in enabledPaths)
{
if (!candidates.ContainsKey(enabledPath))
{
candidates[enabledPath] = enabledPath;
}
}
return candidates
.Select(entry => new ActionBarInputRow(
entry.Key,
entry.Value,
enabledPaths.Contains(entry.Key)))
.ToArray();
}
private static string ReadInputLabel(
string inputPath,
IEnumerable<AetheriaRuntimeObservedInputBinding> observedBindings)
{
foreach (var binding in observedBindings ?? Array.Empty<AetheriaRuntimeObservedInputBinding>())
{
if (binding == null ||
!string.Equals(binding.InputPath, inputPath, StringComparison.Ordinal) ||
string.IsNullOrWhiteSpace(binding.InputLabel))
{
continue;
}
return binding.InputLabel;
}
return inputPath;
}
private static AetheriaRuntimeSurfaceDocument Build(
IReadOnlyList<InputBindingRow> bindings,
IReadOnlyList<ActionBarInputRow> actionBarInputs,
bool capturePending,
string capturePrompt,
string updatedAtUtc,
long version)
{
bindings ??= Array.Empty<InputBindingRow>();
actionBarInputs ??= Array.Empty<ActionBarInputRow>();
return new AetheriaRuntimeSurfaceDocument(
providerId: "aetheria",
providerKind: "game.runtime",
title: "Aetheria Input Settings",
version: version,
updatedAtUtc: updatedAtUtc ?? "",
surface: new AetheriaRuntimeSurfaceTree(
AetheriaRuntimeInputSettingsCommands.SurfaceId,
Node(
"aetheria.inputSettings.root",
"surface",
Array.Empty<(string Key, string Value)>(),
Node(
"aetheria.inputSettings.summary",
"card",
new[] { ("title", "Input Settings") },
Metric(
"aetheria.inputSettings.summary.bindings",
"Rebindable Inputs",
bindings.Count.ToString()),
Metric(
"aetheria.inputSettings.summary.actionBar",
"Action-Bar Inputs",
actionBarInputs.Count(entry => entry.Enabled).ToString()),
Text(
"aetheria.inputSettings.summary.note",
"Low-level InputSystem edits flow through this Eve surface as typed input-setting requests.")),
Node(
"aetheria.inputSettings.capture",
"card",
new[] { ("title", "Capture") },
Text(
"aetheria.inputSettings.capture.note",
capturePending
? capturePrompt ?? ""
: "Choose a binding row, then press a keyboard or mouse input to capture it."),
ButtonRow(
"aetheria.inputSettings.capture.actions",
capturePending
? Button(
"aetheria.inputSettings.capture.cancel",
"Cancel Capture",
AetheriaRuntimeInputSettingsCommands.CancelCapture)
: Button(
"aetheria.inputSettings.capture.refresh",
"Refresh",
AetheriaRuntimeInputSettingsCommands.Refresh))),
Node(
"aetheria.inputSettings.bindings",
"card",
new[] { ("title", "Bindings") },
Node(
"aetheria.inputSettings.bindings.grid",
"grid",
Array.Empty<(string Key, string Value)>(),
bindings.Select(BuildBindingCard).ToArray())),
Node(
"aetheria.inputSettings.actionBarInputs",
"card",
new[] { ("title", "Action-Bar Inputs") },
Node(
"aetheria.inputSettings.actionBarInputs.grid",
"grid",
Array.Empty<(string Key, string Value)>(),
actionBarInputs.Select(BuildActionBarCard).ToArray()))),
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
commands: new[]
{
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeInputSettingsCommands.Refresh,
"Refresh",
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeInputSettingsCommands.BeginCapture,
"Capture Binding",
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeInputSettingsCommands.CancelCapture,
"Cancel Capture",
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeInputSettingsCommands.ToggleActionBar,
"Toggle Action-Bar Input",
AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport)
});
}
private static AetheriaRuntimeSurfaceComponent BuildBindingCard(
InputBindingRow binding)
{
return Node(
$"aetheria.inputSettings.binding.{binding.ActionName}.{binding.BindingIndex}",
"card",
new[] { ("title", binding.BindingLabel) },
Metric(
$"aetheria.inputSettings.binding.{binding.ActionName}.{binding.BindingIndex}.current",
"Current Input",
binding.CurrentInputLabel),
Button(
$"aetheria.inputSettings.binding.{binding.ActionName}.{binding.BindingIndex}.capture",
"Capture Binding",
AetheriaRuntimeInputSettingsCommands.BeginCapture,
("actionName", binding.ActionName),
("bindingIndex", binding.BindingIndex.ToString()),
("bindingLabel", binding.BindingLabel)));
}
private static AetheriaRuntimeSurfaceComponent BuildActionBarCard(
ActionBarInputRow input)
{
return Node(
$"aetheria.inputSettings.actionBar.{input.Label}",
"card",
new[] { ("title", input.Label) },
Metric(
$"aetheria.inputSettings.actionBar.{input.Label}.state",
"Enabled",
input.Enabled ? "Yes" : "No"),
Button(
$"aetheria.inputSettings.actionBar.{input.Label}.toggle",
input.Enabled ? "Disable" : "Enable",
AetheriaRuntimeInputSettingsCommands.ToggleActionBar,
("inputPath", input.InputPath),
("enabled", input.Enabled ? "false" : "true"),
("label", input.Label)));
}
private static AetheriaRuntimeSurfaceComponent Metric(string id, string label, string value)
{
return Node(id, "metric", new[] { ("label", label), ("value", value) });
}
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,
params (string Key, string Value)[] extraProps)
{
var props = new List<(string Key, string Value)>
{
("label", label),
("command", command)
};
if (extraProps != null)
{
props.AddRange(extraProps);
}
return Node(id, "control.button", props);
}
private static AetheriaRuntimeSurfaceComponent ButtonRow(
string id,
params AetheriaRuntimeSurfaceComponent[] children)
{
return Node(id, "row", 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.ToDictionary(prop => prop.Key, prop => prop.Value, StringComparer.Ordinal),
children ?? Array.Empty<AetheriaRuntimeSurfaceComponent>());
}
private sealed class InputBindingRow
{
public InputBindingRow(
string actionName,
int bindingIndex,
string bindingLabel,
string currentInputLabel)
{
ActionName = actionName ?? "";
BindingIndex = bindingIndex;
BindingLabel = bindingLabel ?? "";
CurrentInputLabel = currentInputLabel ?? "";
}
public string ActionName { get; }
public int BindingIndex { get; }
public string BindingLabel { get; }
public string CurrentInputLabel { get; }
}
private sealed class ActionBarInputRow
{
public ActionBarInputRow(string inputPath, string label, bool enabled)
{
InputPath = inputPath ?? "";
Label = label ?? "";
Enabled = enabled;
}
public string InputPath { get; }
public string Label { get; }
public bool Enabled { get; }
}
}
public enum AetheriaRuntimeInputSettingsCommandKind
{
Unknown = 0,
Refresh = 1,
BeginCapture = 2,
CancelCapture = 3,
ToggleActionBar = 4
}
public readonly struct AetheriaRuntimeInputSettingsSurfaceCommand
{
public AetheriaRuntimeInputSettingsSurfaceCommand(
AetheriaRuntimeInputSettingsCommandKind kind,
string actionName = "",
int bindingIndex = -1,
string bindingLabel = "",
string inputPath = "",
bool enabled = false)
{
Kind = kind;
ActionName = actionName ?? "";
BindingIndex = bindingIndex;
BindingLabel = bindingLabel ?? "";
InputPath = inputPath ?? "";
Enabled = enabled;
}
public AetheriaRuntimeInputSettingsCommandKind Kind { get; }
public string ActionName { get; }
public int BindingIndex { get; }
public string BindingLabel { get; }
public string InputPath { get; }
public bool Enabled { get; }
}
public static class AetheriaRuntimeInputSettingsSurfaceCommands
{
public static bool TryRead(
EveSurfaceCommandRequest request,
out AetheriaRuntimeInputSettingsSurfaceCommand command)
{
command = default;
if (request == null ||
!string.Equals(request.SurfaceId, AetheriaRuntimeInputSettingsCommands.SurfaceId, StringComparison.Ordinal))
return false;
switch (request.Operation?.OperationId ?? "")
{
case AetheriaRuntimeInputSettingsCommands.Refresh:
command = new AetheriaRuntimeInputSettingsSurfaceCommand(
AetheriaRuntimeInputSettingsCommandKind.Refresh);
return true;
case AetheriaRuntimeInputSettingsCommands.CancelCapture:
command = new AetheriaRuntimeInputSettingsSurfaceCommand(
AetheriaRuntimeInputSettingsCommandKind.CancelCapture);
return true;
case AetheriaRuntimeInputSettingsCommands.BeginCapture:
var actionName = ReadString(request, "actionName");
command = new AetheriaRuntimeInputSettingsSurfaceCommand(
AetheriaRuntimeInputSettingsCommandKind.BeginCapture,
actionName: actionName,
bindingIndex: ReadInt(request, "bindingIndex", -1),
bindingLabel: ReadString(request, "bindingLabel", actionName));
return true;
case AetheriaRuntimeInputSettingsCommands.ToggleActionBar:
command = new AetheriaRuntimeInputSettingsSurfaceCommand(
AetheriaRuntimeInputSettingsCommandKind.ToggleActionBar,
inputPath: ReadString(request, "inputPath"),
enabled: ReadBool(request, "enabled", false));
return true;
default:
return false;
}
}
private static string ReadString(EveSurfaceCommandRequest request, string key, string defaultValue = "")
{
return request.Payload.GetString(key, defaultValue);
}
private static int ReadInt(EveSurfaceCommandRequest request, string key, int defaultValue)
{
return request.Payload.GetInt32(key, defaultValue);
}
private static bool ReadBool(EveSurfaceCommandRequest request, string key, bool defaultValue)
{
return request.Payload.GetBoolean(key, defaultValue);
}
}
}