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 DefaultActionBarCandidatePaths = new[] { "/leftButton", "/rightButton", "/middleButton", "/forwardButton", "/backButton", "/1", "/2", "/3", "/4", "/5", "/leftShift" }; public static readonly IReadOnlyList DefaultActionBarCandidateInputPaths = new[] { new AetheriaRuntimeInputPathSurfaceLabel("/leftButton", "Left Button"), new AetheriaRuntimeInputPathSurfaceLabel("/rightButton", "Right Button"), new AetheriaRuntimeInputPathSurfaceLabel("/middleButton", "Middle Button"), new AetheriaRuntimeInputPathSurfaceLabel("/forwardButton", "Forward Button"), new AetheriaRuntimeInputPathSurfaceLabel("/backButton", "Back Button"), new AetheriaRuntimeInputPathSurfaceLabel("/1", "1"), new AetheriaRuntimeInputPathSurfaceLabel("/2", "2"), new AetheriaRuntimeInputPathSurfaceLabel("/3", "3"), new AetheriaRuntimeInputPathSurfaceLabel("/4", "4"), new AetheriaRuntimeInputPathSurfaceLabel("/5", "5"), new AetheriaRuntimeInputPathSurfaceLabel("/leftShift", "Left Shift") }; public static bool IsSupportedCapturePath(string path) { return !string.IsNullOrWhiteSpace(path) && (path.StartsWith("/", StringComparison.Ordinal) || path.StartsWith("/", StringComparison.Ordinal)); } public static AetheriaRuntimeSurfaceDocument Build( IEnumerable observedBindings, IEnumerable enabledActionBarInputPaths, bool capturePending, string capturePrompt, string updatedAtUtc, long version = 1) { var observed = (observedBindings ?? Array.Empty()) .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 ComposeBindingInputs( IEnumerable observedBindings) { return (observedBindings ?? Array.Empty()) .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 ComposeActionBarCandidates( IEnumerable observedBindings, IEnumerable enabledInputPaths) { var candidates = new List(); candidates.AddRange(DefaultActionBarCandidateInputPaths); foreach (var inputPath in enabledInputPaths ?? Array.Empty()) { if (!string.IsNullOrWhiteSpace(inputPath)) { candidates.Add(new AetheriaRuntimeInputPathSurfaceLabel( inputPath, ReadInputLabel(inputPath, observedBindings))); } } foreach (var binding in observedBindings ?? Array.Empty()) { 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 ComposeActionBarInputs( IEnumerable enabledInputPaths, IEnumerable candidateInputPaths) { var enabledPaths = new HashSet( (enabledInputPaths ?? Array.Empty()).Where(path => !string.IsNullOrWhiteSpace(path)), StringComparer.Ordinal); var candidates = new SortedDictionary(StringComparer.Ordinal); foreach (var candidate in candidateInputPaths ?? Array.Empty()) { 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 observedBindings) { foreach (var binding in observedBindings ?? Array.Empty()) { 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 bindings, IReadOnlyList actionBarInputs, bool capturePending, string capturePrompt, string updatedAtUtc, long version) { bindings ??= Array.Empty(); actionBarInputs ??= Array.Empty(); 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()), 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()); } 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); } } }