Move input rebinding to Eve UI Toolkit

This commit is contained in:
Meta Vens 2026-06-17 08:41:06 +01:00
commit cbbc6fe12d
8 changed files with 743 additions and 479 deletions

View file

@ -1,573 +1,438 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using TMPro;
using UniRx;
using GameCult.Aetheria.State.Unity;
using GameCult.Eve.Surface;
using GameCult.Eve.UnityUIToolkit;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using UnityEngine.UI.Extensions;
using Unity.Mathematics;
using UnityEngine.EventSystems;
using static Unity.Mathematics.math;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UIElements;
public class InputDisplayLayout : MonoBehaviour
{
public RectTransform UnassignedBindingsGroup;
public RectTransform AssignedBindingsGroup;
public InputDisplayButton MouseLeft;
public InputDisplayButton MouseRight;
public InputDisplayButton MouseMiddle;
public InputDisplayButton MouseForward;
public InputDisplayButton MouseBack;
public VerticalLayoutGroup RowContainer;
public Prototype LabelPrototype;
public Prototype LinePrototype;
//public UILineRendererList LineRenderer;
public Prototype RowPrototype;
public Prototype RowSpacerPrototype;
public int KeySize = 64;
public Color DefaultColor;
public Color HighlightColor;
public float FillBrightness = .5f;
public float FillAlpha;
public float FillMultiplier = .25f;
public float InactiveMultiplier = .333f;
public int PlacementSearchArea = 4;
public float TestPadding = 4;
public float BoxLineExpand = -1;
public float GlobalHue = .333f;
public float Saturation;
public float GlobalHueRange = .1f;
private Canvas _canvas;
private InputLayout _inputLayout;
private ActionMapping _dragAction;
private ButtonMapping _originalButton;
private ButtonMapping _previewButton;
private ActionMapping _previewOriginalAction;
private bool _previewOriginallyActionBar;
public InputActionAsset Input { get; set; }
private class ButtonMapping
private static readonly string[] DefaultActionBarCandidatePaths =
{
public InputAction TestAction;
public IBindableButton Button;
public InputDisplayButton DisplayButton;
public ActionMapping ActionMapping;
public UILineRenderer LabelLine;
public bool IsActionBarButton;
public Rect ButtonRect => DisplayButton.GetComponent<RectTransform>().ScreenSpaceRect();
}
"<Mouse>/leftButton",
"<Mouse>/rightButton",
"<Mouse>/middleButton",
"<Mouse>/forwardButton",
"<Mouse>/backButton",
"<Keyboard>/1",
"<Keyboard>/2",
"<Keyboard>/3",
"<Keyboard>/4",
"<Keyboard>/5",
"<Keyboard>/leftShift"
};
private class ActionMapping
private UIDocument _surfaceDocument;
private AetheriaInput _ownedInput;
private InputAction _captureAction;
private InputActionAsset _input;
private string _captureActionName = "";
private int _captureBindingIndex = -1;
private string _captureBindingLabel = "";
public InputActionAsset Input
{
public string Name;
public InputBinding Binding;
public InputAction Action;
public InputDisplayLabel Label;
public float Hue;
public List<ButtonMapping> ButtonMappings = new List<ButtonMapping>();
public Rect LabelRect => Label.GetComponent<RectTransform>().ScreenSpaceRect();
}
private Dictionary<string, ButtonMapping> _bindButtons = new Dictionary<string, ButtonMapping>();
private List<ButtonMapping> _buttonMappings = new List<ButtonMapping>();
private List<ActionMapping> _actionMappings = new List<ActionMapping>();
void Start()
{
_canvas = transform.root.GetComponent<Canvas>();
_inputLayout = InputLayout.CreateAnsi104();
DisplayLayout(_inputLayout);
_buttonMappings.Add(MapMouseButton(MouseLeft, "<Mouse>/leftButton"));
_buttonMappings.Add(MapMouseButton(MouseRight, "<Mouse>/rightButton"));
_buttonMappings.Add(MapMouseButton(MouseMiddle, "<Mouse>/middleButton"));
_buttonMappings.Add(MapMouseButton(MouseForward, "<Mouse>/forwardButton"));
_buttonMappings.Add(MapMouseButton(MouseBack, "<Mouse>/backButton"));
Canvas.ForceUpdateCanvases();
foreach (var buttonMapping in _buttonMappings)
get => _input;
set
{
_bindButtons[buttonMapping.Button.InputSystemPath] = buttonMapping;
buttonMapping.TestAction = new InputAction(binding: buttonMapping.Button.InputSystemPath);
buttonMapping.TestAction.started += context =>
_input = value;
if (isActiveAndEnabled)
{
buttonMapping.DisplayButton.Outline.color = HighlightColor;
buttonMapping.DisplayButton.Outline.gameObject.SetActive(true);
var fillColor = HighlightColor;
fillColor *= FillBrightness;
fillColor.a = FillAlpha;
buttonMapping.DisplayButton.Fill.color = fillColor;
};
buttonMapping.TestAction.canceled += context => AssignColor(buttonMapping);
buttonMapping.TestAction.Enable();
}
foreach(var actionBarInput in ActionGameManager.RuntimePlayerSettings.InputSettings.ActionBarInputs)
{
if (!_bindButtons.ContainsKey(actionBarInput)) Debug.LogError($"Unable to find input button for \"{actionBarInput}\"");
else
{
_bindButtons[actionBarInput].IsActionBarButton = true;
AssignColor(_bindButtons[actionBarInput]);
}
}
if(Input==null)
{
var input = new AetheriaInput();
Input = input.asset;
}
ProcessActions(Input);
// input.Enable();
// input.Global.Interact.performed += context => Debug.Log($"Interact Performed! Time = {((int) (Time.time * 1000)).ToString()}ms");
Observable.NextFrame().Subscribe(_ =>
{
foreach (var actionMapping in _actionMappings)
{
actionMapping.Label = CreateLabel(actionMapping);
PlaceLabel(actionMapping);
}
RegisterMouseCallbacks();
});
//StartCoroutine(AssociateInputKeys(_inputLayout));
}
private ButtonMapping MapMouseButton(InputDisplayButton button, string path)
{
var mapping = new ButtonMapping
{
Button = new InputLayoutMouseButton {Path = path},
DisplayButton = button
};
AssignUnboundColor(mapping.DisplayButton);
return mapping;
}
private void ProcessActions(InputActionAsset input)
{
var bindableActions = input.Where(a => a.actionMap.name != "UI" && (a.type==InputActionType.Button || a.bindings.Any(b=>b.isComposite))).ToArray();
for (var i = 0; i < bindableActions.Length; i++)
{
var action = bindableActions[i];
foreach (var binding in action.bindings)
{
if (_bindButtons.ContainsKey(binding.effectivePath))
{
var name = action.name.Replace(' ', '\n');
if (binding.isPartOfComposite) name = $"{name} {binding.name}";
var actionMapping = new ActionMapping
{
Name = name,
Binding = binding,
Action = action,
Hue = action.actionMap.name == "Global"
? GlobalHue
: frac(GlobalHue + GlobalHueRange + (float) i / (bindableActions.Length - 1) * (1 - GlobalHueRange * 2))
};
_actionMappings.Add(actionMapping);
var buttonMapping = _bindButtons[binding.effectivePath];
buttonMapping.ActionMapping = actionMapping;
AssignColor(buttonMapping);
actionMapping.ButtonMappings.Add(buttonMapping);
}
RenderSurface();
}
}
}
private void AssignColor(ButtonMapping buttonMapping)
private void Start()
{
if (buttonMapping.IsActionBarButton)
{
var outlineColor = Color.white;
var fillColor = Color.white * FillBrightness;
fillColor.a = FillAlpha;
buttonMapping.DisplayButton.Fill.color = fillColor;
buttonMapping.DisplayButton.Outline.gameObject.SetActive(true);
buttonMapping.DisplayButton.Outline.color = outlineColor;
if (buttonMapping.DisplayButton is InputDisplayKey key) key.MainLabel.color = key.AltLabel.color = outlineColor;
}
else if(buttonMapping.ActionMapping!=null)
{
var outlineColor = Color.HSVToRGB(buttonMapping.ActionMapping.Hue, Saturation, 1);
var fillColor = Color.HSVToRGB(buttonMapping.ActionMapping.Hue, 1, FillBrightness);
fillColor.a = FillAlpha;
buttonMapping.DisplayButton.Fill.color = fillColor;
buttonMapping.DisplayButton.Outline.gameObject.SetActive(true);
buttonMapping.DisplayButton.Outline.color = outlineColor;
if (buttonMapping.DisplayButton is InputDisplayKey key) key.MainLabel.color = key.AltLabel.color = outlineColor;
}
else AssignUnboundColor(buttonMapping.DisplayButton);
}
private void AssignUnboundColor(InputDisplayButton button, bool bindable = true)
{
button.Outline.gameObject.SetActive(false);
var outlineColor = DefaultColor;
outlineColor.a *= InactiveMultiplier;
var fillColor = DefaultColor * FillBrightness;
fillColor *= FillBrightness;
if(!bindable)
fillColor *= InactiveMultiplier;
fillColor.a = FillAlpha;
button.Fill.color = fillColor;
if (button is InputDisplayKey key) key.MainLabel.color = key.AltLabel.color = Color.Lerp(outlineColor, fillColor, .5f);
}
private InputDisplayLabel CreateLabel(ActionMapping actionMapping)
{
var label = LabelPrototype.Instantiate<InputDisplayLabel>();
label.Label.color = Color.HSVToRGB(actionMapping.Hue, Saturation, 1);
actionMapping.Label = label;
label.Label.text = actionMapping.Name;
return label;
}
private void PlaceLabel(ActionMapping actionMapping)
{
if(actionMapping.ButtonMappings.Any())
{
var labelRect = actionMapping.Label.Label.rectTransform;
LayoutRebuilder.ForceRebuildLayoutImmediate(labelRect);
var rect = FindLabelPosition(actionMapping.ButtonMappings.First().DisplayButton.GetComponent<RectTransform>(), labelRect);
//actionMapping.LabelRect = rect;
actionMapping.Label.transform.parent = AssignedBindingsGroup;
actionMapping.Label.transform.position = rect.center;
}
else
{
actionMapping.Label.transform.parent = UnassignedBindingsGroup;
}
foreach(var buttonMapping in actionMapping.ButtonMappings) ConnectButtonToLabel(buttonMapping);
}
private void ConnectButtonToLabel(ButtonMapping buttonMapping)
{
if (buttonMapping.ActionMapping != null)
{
var keyBounds = buttonMapping.DisplayButton.Outline.rectTransform.GetBounds(BoxLineExpand);
var labelPoint = (Vector2) buttonMapping.ActionMapping.Label.Label.rectTransform.GetBounds().ClosestPoint(keyBounds.center);
var keyPoint = (Vector2) keyBounds.ClosestPoint(labelPoint);
labelPoint /= _canvas.scaleFactor;
keyPoint /= _canvas.scaleFactor;
buttonMapping.LabelLine ??= LinePrototype.Instantiate<UILineRenderer>();
buttonMapping.LabelLine.color = buttonMapping.ActionMapping.Label.Label.color;
buttonMapping.LabelLine.Points = new[] {keyPoint, labelPoint};
}
else
{
buttonMapping.LabelLine?.GetComponent<Prototype>().ReturnToPool();
buttonMapping.LabelLine = null;
}
}
private Rect FindLabelPosition(RectTransform key, RectTransform label)
{
var keyRect = key.ScreenSpaceRect();
var labelRect = label.ScreenSpaceRect(TestPadding*_canvas.scaleFactor);
var localRects = Overlap(key.ScreenSpaceRect(PlacementSearchArea * KeySize * _canvas.scaleFactor));
var centroid = localRects.Aggregate(Vector2.zero, (v, r) => v + r.center) / localRects.Length;
var keyCenter = keyRect.center;
var dir = (keyCenter - centroid).normalized;
for (int dist = KeySize; dist < PlacementSearchArea * KeySize; dist++)
{
for (float theta = 0; theta < PI * 2; theta += PI / 32)
{
for (int s = -1; s <= 1; s += 2)
{
var testPos = keyCenter + dir.Rotate(theta * s) * dist * _canvas.scaleFactor;
var testRect = new Rect(testPos - labelRect.size / 2, labelRect.size);
if(!localRects.Any(r => r.Overlaps(testRect)))
{
return testRect;
}
}
}
}
return default;
}
private ActionMapping[] OverlappingLabels(Rect rect) => _actionMappings
.Where(a => a.LabelRect.Overlaps(rect)).ToArray();
private Rect[] Overlap(Rect rect) => _buttonMappings
.Where(b=>b.IsActionBarButton || b.ActionMapping!=null)
.Select(b=>b.ButtonRect)
.Concat(_actionMappings
.Where(a=>a.Label && a.LabelRect.width > .1f)
.Select(a=>a.LabelRect))
.Where(r => r.Overlaps(rect)).ToArray();
public void DisplayLayout(InputLayout layout)
{
foreach (var row in layout.Rows)
{
if (row is InputLayoutKeyRow keyRow)
{
var displayRow = RowPrototype.Instantiate<InputDisplayRow>();
foreach (var column in keyRow.Columns)
{
if (column is InputLayoutColumnSpacer)
{
displayRow.KeySpacerPrototype.Instantiate<LayoutElement>().preferredWidth = KeySize * column.Width;
}
else if (column is InputLayoutKey)
{
var displayKey = displayRow.KeyPrototype.Instantiate<InputDisplayKey>();
displayKey.LayoutElement.preferredHeight = KeySize;
displayKey.LayoutElement.preferredWidth = KeySize * column.Width;
displayKey.Outline.gameObject.SetActive(false);
displayKey.Outline.color = DefaultColor;
AssignUnboundColor(displayKey, column is InputLayoutBindableKey);
if (column is InputLayoutBindableKey key)
{
var buttonMapping = new ButtonMapping
{
Button = key,
DisplayButton = displayKey
};
_bindButtons[key.InputSystemPath] = buttonMapping;
_buttonMappings.Add(buttonMapping);
displayKey.MainLabel.text = key.MainLabel;
displayKey.AltLabel.text = key.AltLabel;
if (column is InputLayoutMultiRowKey multiRowKey)
{
displayKey.Outline.rectTransform.anchorMin =
displayKey.MainLabel.rectTransform.anchorMin =
displayKey.AltLabel.rectTransform.anchorMin =
displayKey.Fill.rectTransform.anchorMin = Vector2.down * (multiRowKey.Height - 1);
}
}
else
{
displayKey.MainLabel.text = "";
displayKey.AltLabel.text = "";
}
}
else
{
throw new ArgumentOutOfRangeException(nameof(column));
}
}
}
else if (row is InputLayoutRowSpacer rowSpacer)
{
RowSpacerPrototype.Instantiate<LayoutElement>().preferredHeight = KeySize * rowSpacer.Height;
}
else
{
throw new ArgumentOutOfRangeException(nameof(row));
}
}
}
private IEnumerator AssociateInputKeys()
{
var path = "";
var keyPress = new InputAction(binding: "/<Keyboard>/<button>");
keyPress.performed += context =>
{
if (context.control.path.EndsWith("anyKey")) return;
path = context.control.path;
};
keyPress.Enable();
foreach (var button in _buttonMappings)
{
button.DisplayButton.Outline.color = HighlightColor;
var fillColor = HighlightColor;
fillColor *= FillMultiplier;
fillColor.a = FillAlpha;
button.DisplayButton.Fill.color = fillColor;
path = "";
while (string.IsNullOrEmpty(path)) yield return null;
button.Button.InputSystemPath = path;
//Debug.Log($"Bound \"{path}\" to \"{bindableKey.MainLabel}\"");
button.DisplayButton.Outline.color = DefaultColor;
fillColor = DefaultColor;
fillColor *= FillMultiplier;
fillColor.a = FillAlpha;
button.DisplayButton.Fill.color = fillColor;
}
keyPress.Disable();
QueueRuntimeInputSettingsCommit();
}
private void QueueRuntimeInputSettingsCommit()
{
ActionGameManager.QueueRuntimePlayerSettingsCommit();
}
private void CommitBindingOverride(InputAction action, InputBinding binding, string inputSystemPath)
{
var bindingIndex = action.GetBindingIndex(binding);
action.ApplyBindingOverride(bindingIndex, inputSystemPath);
ActionGameManager.CommitRuntimeInputBindingOverride(action.name, bindingIndex, inputSystemPath);
EnsureInputAsset();
EnsureSurfaceDocument();
EnsureCaptureAction();
HideLegacyChildren();
RenderSurface();
}
private void OnEnable()
{
foreach(var buttonMapping in _buttonMappings)
buttonMapping.TestAction.Enable();
RegisterMouseCallbacks();
}
private void RegisterMouseCallbacks()
{
void EndDrag(PointerEventData _)
{
if (_previewButton == null)
{
if(_originalButton!=null)
{
_originalButton.ActionMapping = _dragAction;
_dragAction.ButtonMappings.Add(_originalButton);
AssignColor(_originalButton);
}
PlaceLabel(_dragAction);
}
else
{
CommitBindingOverride(_dragAction.Action, _dragAction.Binding, _previewButton.Button.InputSystemPath);
}
foreach(var action in _actionMappings)
action.Label.Label.raycastTarget = true;
_dragAction = null;
_originalButton = null;
_previewButton = null;
_previewOriginalAction = null;
}
foreach (var buttonMapping in _buttonMappings)
{
// On click: when a specific action is not assigned, toggle its availability on the action bar
buttonMapping.DisplayButton.ClickTrigger.OnPointerClickAsObservable()
.Where(_=>buttonMapping.ActionMapping==null)
.Subscribe(data =>
{
buttonMapping.IsActionBarButton = !buttonMapping.IsActionBarButton;
ActionGameManager.CommitRuntimeActionBarInput(buttonMapping.Button.InputSystemPath, buttonMapping.IsActionBarButton);
if (buttonMapping.IsActionBarButton)
foreach(var overlap in OverlappingLabels(buttonMapping.ButtonRect))
PlaceLabel(overlap);
AssignColor(buttonMapping);
});
buttonMapping.DisplayButton.BeginDragTrigger.OnBeginDragAsObservable()
.Where(_ => buttonMapping.ActionMapping != null)
.Subscribe(_ =>
{
_originalButton = buttonMapping;
buttonMapping.LabelLine.Points = new Vector2[0];
_dragAction = buttonMapping.ActionMapping;
foreach(var action in _actionMappings)
action.Label.Label.raycastTarget = false;
});
buttonMapping.DisplayButton.EnterTrigger.OnPointerEnterAsObservable()
.Where(_ => _dragAction != null)
.Subscribe(_ =>
{
_previewButton = buttonMapping;
_previewOriginalAction = buttonMapping.ActionMapping;
_previewOriginallyActionBar = buttonMapping.IsActionBarButton;
buttonMapping.IsActionBarButton = false;
buttonMapping.ActionMapping?.ButtonMappings.Remove(buttonMapping);
buttonMapping.ActionMapping = _dragAction;
_dragAction.ButtonMappings.Add(buttonMapping);
if(_previewOriginalAction!=null)
PlaceLabel(_previewOriginalAction);
PlaceLabel(_dragAction);
AssignColor(buttonMapping);
foreach(var overlap in OverlappingLabels(buttonMapping.ButtonRect))
PlaceLabel(overlap);
});
buttonMapping.DisplayButton.ExitTrigger.OnPointerExitAsObservable()
.Where(_ => _dragAction != null)
.Subscribe(_ =>
{
_dragAction.ButtonMappings.Remove(buttonMapping);
if (buttonMapping == _originalButton)
{
buttonMapping.ActionMapping = null;
}
else if(buttonMapping == _previewButton)
{
buttonMapping.IsActionBarButton = _previewOriginallyActionBar;
buttonMapping.ActionMapping = _previewOriginalAction;
if (!_previewOriginallyActionBar)
{
if(_previewOriginalAction != null)
{
_previewOriginalAction.ButtonMappings.Add(buttonMapping);
PlaceLabel(_previewOriginalAction);
}
}
}
ConnectButtonToLabel(buttonMapping);
AssignColor(buttonMapping);
_previewButton = null;
_previewOriginalAction = null;
});
buttonMapping.DisplayButton.EndDragTrigger.OnEndDragAsObservable()
.Where(_ => _dragAction != null)
.Subscribe(EndDrag);
}
foreach (var actionMapping in _actionMappings)
{
actionMapping.Label.BeginDragTrigger.OnBeginDragAsObservable().Subscribe(_ =>
{
_dragAction = actionMapping;
actionMapping.Label.transform.parent = AssignedBindingsGroup;
foreach (var action in _actionMappings)
action.Label.Label.raycastTarget = false;
if (actionMapping.ButtonMappings.Count == 1)
{
var buttonMapping = actionMapping.ButtonMappings[0];
_originalButton = buttonMapping;
buttonMapping.LabelLine.Points = new Vector2[0];
buttonMapping.ActionMapping = null;
actionMapping.ButtonMappings.Clear();
ConnectButtonToLabel(buttonMapping);
AssignColor(buttonMapping);
}
});
actionMapping.Label.EndDragTrigger.OnEndDragAsObservable().Subscribe(EndDrag);
}
EnsureInputAsset();
EnsureSurfaceDocument();
EnsureCaptureAction();
HideLegacyChildren();
RenderSurface();
}
private void OnDisable()
{
foreach(var buttonMapping in _buttonMappings)
buttonMapping.TestAction.Disable();
}
void Update()
{
if (_dragAction != null && _previewButton == null)
ClearCapture();
if (_captureAction != null)
{
_dragAction.Label.transform.position = Mouse.current.position.ReadValue();
_captureAction.Disable();
}
if (_surfaceDocument != null)
{
_surfaceDocument.rootVisualElement.Clear();
}
}
private void OnDestroy()
{
_captureAction?.Dispose();
_captureAction = null;
_ownedInput?.Dispose();
_ownedInput = null;
if (_surfaceDocument != null)
{
Destroy(_surfaceDocument.gameObject);
_surfaceDocument = null;
}
}
private void EnsureInputAsset()
{
if (Input != null)
{
return;
}
_ownedInput ??= new AetheriaInput();
Input = _ownedInput.asset;
}
private void EnsureSurfaceDocument()
{
if (_surfaceDocument != null)
{
return;
}
var host = new GameObject("Aetheria Input Surface");
host.transform.SetParent(transform, false);
host.layer = gameObject.layer;
_surfaceDocument = host.AddComponent<UIDocument>();
_surfaceDocument.sortingOrder = 1000;
}
private void HideLegacyChildren()
{
for (var index = 0; index < transform.childCount; index++)
{
var child = transform.GetChild(index);
if (_surfaceDocument != null && child.gameObject == _surfaceDocument.gameObject)
{
continue;
}
child.gameObject.SetActive(false);
}
}
private void EnsureCaptureAction()
{
if (_captureAction != null)
{
return;
}
_captureAction = new InputAction("Aetheria Input Capture");
_captureAction.AddBinding("<Keyboard>/anyKey");
_captureAction.AddBinding("<Mouse>/leftButton");
_captureAction.AddBinding("<Mouse>/rightButton");
_captureAction.AddBinding("<Mouse>/middleButton");
_captureAction.AddBinding("<Mouse>/forwardButton");
_captureAction.AddBinding("<Mouse>/backButton");
_captureAction.performed += OnCapturePerformed;
}
private void RenderSurface()
{
if (!isActiveAndEnabled)
{
return;
}
EnsureInputAsset();
EnsureSurfaceDocument();
EnsureCaptureAction();
var document = ToEveSurfaceDocument(
AetheriaRuntimeInputSettingsSurfaceBuilder.Build(ProjectSurfaceState()));
var root = _surfaceDocument.rootVisualElement;
root.Clear();
root.style.flexGrow = 1;
root.style.justifyContent = Justify.Center;
root.style.alignItems = Align.Center;
root.style.paddingLeft = 24;
root.style.paddingRight = 24;
root.style.paddingTop = 24;
root.style.paddingBottom = 24;
root.style.backgroundColor = new Color(0f, 0f, 0f, 0.72f);
var shell = new VisualElement();
shell.style.flexDirection = FlexDirection.Column;
shell.style.width = 1080;
shell.style.maxWidth = 1080;
shell.style.maxHeight = 900;
shell.style.flexGrow = 1;
shell.style.paddingLeft = 20;
shell.style.paddingRight = 20;
shell.style.paddingTop = 20;
shell.style.paddingBottom = 20;
shell.style.backgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.96f);
root.Add(shell);
var lowerer = new EveUiToolkitSurfaceLowerer();
shell.Add(lowerer.Lower(document, HandleSurfaceCommand));
}
private AetheriaRuntimeInputSettingsSurfaceState ProjectSurfaceState()
{
return new AetheriaRuntimeInputSettingsSurfaceState(
ProjectBindingRows(),
ProjectActionBarRows(),
capturePending: _captureBindingIndex >= 0 && !string.IsNullOrWhiteSpace(_captureActionName),
capturePrompt: BuildCapturePrompt(),
updatedAtUtc: DateTime.UtcNow.ToString("O"));
}
private IReadOnlyList<AetheriaRuntimeInputBindingSurfaceState> ProjectBindingRows()
{
if (Input == null)
{
return Array.Empty<AetheriaRuntimeInputBindingSurfaceState>();
}
return Input
.Where(action => action.actionMap.name != "UI")
.SelectMany(action =>
action.bindings
.Select((binding, bindingIndex) => new { action, binding, bindingIndex }))
.Where(entry =>
!entry.binding.isComposite &&
!string.IsNullOrWhiteSpace(entry.binding.effectivePath) &&
IsSupportedCapturePath(entry.binding.effectivePath))
.Select(entry => new AetheriaRuntimeInputBindingSurfaceState(
entry.action.name,
entry.bindingIndex,
DescribeBinding(entry.action.name, entry.binding),
DescribeInputPath(entry.binding.effectivePath)))
.OrderBy(entry => entry.BindingLabel, StringComparer.Ordinal)
.ToArray();
}
private IReadOnlyList<AetheriaRuntimeActionBarInputSurfaceState> ProjectActionBarRows()
{
var runtimeSettings = ActionGameManager.RuntimePlayerSettings.InputSettings;
var candidates = new SortedDictionary<string, string>(StringComparer.Ordinal);
foreach (var defaultPath in DefaultActionBarCandidatePaths)
{
candidates[defaultPath] = DescribeInputPath(defaultPath);
}
foreach (var inputPath in runtimeSettings.ActionBarInputs)
{
if (!string.IsNullOrWhiteSpace(inputPath))
{
candidates[inputPath] = DescribeInputPath(inputPath);
}
}
if (Input != null)
{
foreach (var action in Input.Where(action => action.actionMap.name != "UI"))
{
foreach (var binding in action.bindings)
{
if (binding.isComposite || string.IsNullOrWhiteSpace(binding.effectivePath) || !IsSupportedCapturePath(binding.effectivePath))
{
continue;
}
candidates[binding.effectivePath] = DescribeInputPath(binding.effectivePath);
}
}
}
return candidates
.Select(entry => new AetheriaRuntimeActionBarInputSurfaceState(
entry.Key,
entry.Value,
runtimeSettings.ActionBarInputs.Contains(entry.Key)))
.ToArray();
}
private static bool IsSupportedCapturePath(string path)
{
return !string.IsNullOrWhiteSpace(path) &&
(path.StartsWith("<Keyboard>/", StringComparison.Ordinal) ||
path.StartsWith("<Mouse>/", StringComparison.Ordinal));
}
private static string DescribeBinding(string actionName, InputBinding binding)
{
if (binding.isPartOfComposite && !string.IsNullOrWhiteSpace(binding.name))
{
return $"{actionName} {binding.name}";
}
return actionName;
}
private static string DescribeInputPath(string inputPath)
{
if (string.IsNullOrWhiteSpace(inputPath))
{
return "Unbound";
}
var label = InputControlPath.ToHumanReadableString(
inputPath,
InputControlPath.HumanReadableStringOptions.OmitDevice);
return string.IsNullOrWhiteSpace(label) ? inputPath : label;
}
private string BuildCapturePrompt()
{
if (_captureBindingIndex < 0 || string.IsNullOrWhiteSpace(_captureActionName))
{
return "";
}
return $"Capture pending for {_captureBindingLabel}. Press a keyboard or mouse input now.";
}
private void HandleSurfaceCommand(EveSurfaceCommandRequest request)
{
switch (request.Command)
{
case var command when string.Equals(command, AetheriaRuntimeInputSettingsCommands.Refresh, StringComparison.Ordinal):
RenderSurface();
return;
case var command when string.Equals(command, AetheriaRuntimeInputSettingsCommands.CancelCapture, StringComparison.Ordinal):
ClearCapture();
RenderSurface();
return;
case var command when string.Equals(command, AetheriaRuntimeInputSettingsCommands.BeginCapture, StringComparison.Ordinal):
BeginCapture(request.Payload);
return;
case var command when string.Equals(command, AetheriaRuntimeInputSettingsCommands.ToggleActionBar, StringComparison.Ordinal):
ToggleActionBarInput(request.Payload);
return;
default:
Debug.LogWarning($"Unknown input-settings command: {request.Command}");
return;
}
}
private void BeginCapture(IReadOnlyDictionary<string, string> payload)
{
if (payload == null ||
!payload.TryGetValue("actionName", out var actionName) ||
!payload.TryGetValue("bindingIndex", out var bindingIndexText) ||
!int.TryParse(bindingIndexText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var bindingIndex))
{
Debug.LogWarning("Input capture requested without a valid action name and binding index.");
return;
}
_captureActionName = actionName;
_captureBindingIndex = bindingIndex;
_captureBindingLabel = payload.TryGetValue("bindingLabel", out var bindingLabel)
? bindingLabel ?? actionName
: actionName;
_captureAction?.Enable();
RenderSurface();
}
private void ToggleActionBarInput(IReadOnlyDictionary<string, string> payload)
{
if (payload == null ||
!payload.TryGetValue("inputPath", out var inputPath) ||
!payload.TryGetValue("enabled", out var enabledText) ||
!bool.TryParse(enabledText, out var enabled))
{
Debug.LogWarning("Action-bar toggle requested without a valid input path and enabled state.");
return;
}
ActionGameManager.CommitRuntimeActionBarInput(inputPath, enabled);
RenderSurface();
}
private void OnCapturePerformed(InputAction.CallbackContext context)
{
if (_captureBindingIndex < 0 || string.IsNullOrWhiteSpace(_captureActionName))
{
return;
}
var inputPath = context.control?.path ?? "";
if (string.IsNullOrWhiteSpace(inputPath) ||
inputPath.EndsWith("anyKey", StringComparison.Ordinal) ||
!IsSupportedCapturePath(inputPath) ||
Input == null)
{
return;
}
var action = Input.FindAction(_captureActionName, throwIfNotFound: false);
if (action == null || _captureBindingIndex >= action.bindings.Count)
{
Debug.LogWarning($"Input capture target no longer exists for {_captureActionName}:{_captureBindingIndex}.");
ClearCapture();
RenderSurface();
return;
}
action.ApplyBindingOverride(_captureBindingIndex, inputPath);
ActionGameManager.CommitRuntimeInputBindingOverride(action.name, _captureBindingIndex, inputPath);
ClearCapture();
RenderSurface();
}
private void ClearCapture()
{
_captureActionName = "";
_captureBindingIndex = -1;
_captureBindingLabel = "";
}
private static EveSurfaceDocument ToEveSurfaceDocument(AetheriaRuntimeSurfaceDocument document)
{
return new EveSurfaceDocument(
"surface-state",
"gamecult.eve.surface.v1",
document.ProviderId,
document.ProviderKind,
document.Title,
document.Version,
document.UpdatedAtUtc,
new EveSurfaceTree(
document.Surface.Id,
ToEveSurfaceComponent(document.Surface.Root),
document.Surface.Styles
.Select(style => new EveStyleToken(style.Name, style.Value))
.ToArray()),
document.Commands
.Select(command => new EveCommandTemplate(command.Command, command.Label, command.Transport))
.ToArray());
}
private static EveSurfaceComponent ToEveSurfaceComponent(AetheriaRuntimeSurfaceComponent component)
{
return new EveSurfaceComponent(
component.Id,
component.Kind,
new Dictionary<string, string>(component.Props, StringComparer.Ordinal),
component.Children.Select(ToEveSurfaceComponent).ToArray());
}
}