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());
}
}

View file

@ -438,7 +438,7 @@ public class MainMenu : MonoBehaviour
"Settings",
Text(
"aetheria.mainMenu.settings.note",
"Typed Verse settings already lower through Eve. Input rebinding still hands off to the runtime-owned remap screen, and audio still has no typed surface."),
"Typed Verse settings already lower through Eve. Input rebinding now opens the runtime Eve input screen, and audio still has no typed surface."),
ButtonRow(
"aetheria.mainMenu.settings.actions",
Button("aetheria.mainMenu.settings.playerSettings", "Player Settings", ShowPlayerSettingsCommand),
@ -489,19 +489,19 @@ public class MainMenu : MonoBehaviour
commands.Insert(0, new EveCommandTemplate(OpenRuntimeInputScreenCommand, "Open Remap Screen", "unity-uitoolkit"));
cardChildren.Add(Text(
"aetheria.mainMenu.input.note",
"The live remap screen owns drag/drop rebinding and low-level InputSystem edits. This surface reports the typed player-settings state and hands off to that owner."));
"The runtime Eve input screen owns low-level InputSystem rebinding and action-bar input edits. This title shell reports typed player-settings state and hands off to that owner."));
}
else if (inGame)
{
cardChildren.Add(Text(
"aetheria.mainMenu.input.note",
"The live remap screen should own drag/drop rebinding here, but this scene has no active remap surface to hand off to."));
"The runtime Eve input screen should own rebinding here, but this scene has no active input surface to hand off to."));
}
else
{
cardChildren.Add(Text(
"aetheria.mainMenu.input.note",
"This title shell reports the typed player-settings state. Launch a run to open the live remap screen that owns drag/drop rebinding and low-level InputSystem edits."));
"This title shell reports the typed player-settings state. Launch a run to open the runtime Eve input screen that owns low-level InputSystem rebinding."));
}
var buttons = new List<EveSurfaceComponent>();