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

@ -36,6 +36,7 @@ RequireMainMenuRootUsesEveSurface(root);
RequireMainMenuSettingsShellUsesEveSurface(root);
RequireConfirmationDialogOwnsMinimalPromptShell(root);
RequireMainMenuInputSettingsDelegateToRuntimeScreen(root);
RequireRuntimeInputScreenUsesEveSurface(root);
RequireActionGameManagerInputScreenUsesSharedFullscreenPrimitive(root);
RequireSectorMapZoneDetailsUseEveSurface(root);
RequireRuntimeMenuTabsUseEveSurface(root);
@ -516,6 +517,7 @@ Console.WriteLine("Main-menu root shell: root navigation lowers through an Eve U
Console.WriteLine("Main-menu settings shell: settings/input subpages lower through Eve UI Toolkit surfaces, and the fake audio page is deleted until a typed audio owner exists");
Console.WriteLine("Confirmation dialog shell: runtime prompts no longer inherit the generic PropertiesPanel machinery");
Console.WriteLine("Main-menu input shell: the Eve input page delegates to the live runtime remap screen when that owner exists");
Console.WriteLine("Runtime input screen shell: input rebinding lowers through an Eve UI Toolkit surface instead of the old drag/drop uGUI screen");
Console.WriteLine("Runtime input-screen authority: hotkey and menu handoff share the same fullscreen-menu primitive");
Console.WriteLine("Sector-map zone details shell: zone inspection lowers through an Eve UI Toolkit surface instead of PropertiesPanel rows");
Console.WriteLine("Runtime menu tab shell: MenuPanel owns tab metadata and lowers navigation through an Eve UI Toolkit surface");
@ -2783,6 +2785,116 @@ static void RequireMainMenuInputSettingsDelegateToRuntimeScreen(string root)
}
}
static void RequireRuntimeInputScreenUsesEveSurface(string root)
{
var inputScreenPath = Path.Combine(root, "Assets", "Scripts", "UI", "InputScreen", "InputDisplayLayout.cs");
var commandsPath = Path.Combine(
root,
"Packages",
"org.gamecult.aetheria.state",
"Runtime",
"AetheriaRuntimeInputSettingsCommands.cs");
var builderPath = Path.Combine(
root,
"Packages",
"org.gamecult.aetheria.state",
"Runtime",
"AetheriaRuntimeInputSettingsSurfaceBuilder.cs");
if (!File.Exists(inputScreenPath))
{
throw new InvalidOperationException("Cannot verify runtime input-screen Eve lowering; InputDisplayLayout.cs is missing.");
}
if (!File.Exists(commandsPath) || !File.Exists(builderPath))
{
throw new InvalidOperationException(
"Cannot verify runtime input-screen Eve lowering; the shared input-settings Eve contract is missing.");
}
var inputScreen = File.ReadAllText(inputScreenPath);
var commands = File.ReadAllText(commandsPath);
var builder = File.ReadAllText(builderPath);
var requiredInputScreenSymbols = new[]
{
"AetheriaRuntimeInputSettingsSurfaceBuilder.Build(",
"AetheriaRuntimeInputSettingsCommands.BeginCapture",
"AetheriaRuntimeInputSettingsCommands.ToggleActionBar",
"new EveUiToolkitSurfaceLowerer()",
"UIDocument",
"ActionGameManager.CommitRuntimeInputBindingOverride",
"ActionGameManager.CommitRuntimeActionBarInput",
"action.ApplyBindingOverride",
"new InputAction(\"Aetheria Input Capture\")",
"HideLegacyChildren()"
};
var missingInputScreenSymbols = requiredInputScreenSymbols
.Where(symbol => !inputScreen.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingInputScreenSymbols.Length > 0)
{
throw new InvalidOperationException(
"Runtime input screen no longer lowers through the Eve UI Toolkit surface contract: " +
string.Join(", ", missingInputScreenSymbols));
}
var forbiddenInputScreenSymbols = new[]
{
"OnPointerClickAsObservable",
"BeginDragTrigger",
"EndDragTrigger",
"VerticalLayoutGroup",
"LayoutRebuilder",
"UILineRenderer",
"Observable.NextFrame()",
"DisplayLayout(_inputLayout)"
};
var survivingLegacySymbols = forbiddenInputScreenSymbols
.Where(symbol => inputScreen.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (survivingLegacySymbols.Length > 0)
{
throw new InvalidOperationException(
"Runtime input screen still carries the old drag/drop uGUI authority path: " +
string.Join(", ", survivingLegacySymbols));
}
if (!commands.Contains("SurfaceId = \"aetheria.input_settings\"", StringComparison.Ordinal) ||
!commands.Contains("BeginCapture", StringComparison.Ordinal) ||
!commands.Contains("ToggleActionBar", StringComparison.Ordinal) ||
!commands.Contains("public static bool IsKnown", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Shared input-settings command contract is incomplete.");
}
var requiredBuilderSymbols = new[]
{
"public sealed class AetheriaRuntimeInputSettingsSurfaceState",
"public sealed class AetheriaRuntimeInputBindingSurfaceState",
"public sealed class AetheriaRuntimeActionBarInputSurfaceState",
"AetheriaRuntimeInputSettingsCommands.BeginCapture",
"AetheriaRuntimeInputSettingsCommands.ToggleActionBar",
"\"Low-level InputSystem edits flow through this Eve surface and queue typed player-settings commits.\""
};
var missingBuilderSymbols = requiredBuilderSymbols
.Where(symbol => !builder.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingBuilderSymbols.Length > 0)
{
throw new InvalidOperationException(
"Shared input-settings Eve surface builder is incomplete: " +
string.Join(", ", missingBuilderSymbols));
}
}
static void RequireActionGameManagerInputScreenUsesSharedFullscreenPrimitive(string root)
{
var managerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs");

View file

@ -76,8 +76,10 @@ still calls Unity's `InputActionAsset.FromJson`; that is an engine-owned
lowering boundary under Aetheria's remapping layer, not a save format or state
owner. The old generated `GameData/KeyboardLayouts/*.msgpack` cache and the
checked-in `ansi104.json` display file are deleted. Remapping and action-bar
edits are owned by typed `AetheriaPlayerSettings` commits; keyboard display
layout edits remain runtime-only until Verse owns a typed layout document.
edits are owned by typed `AetheriaPlayerSettings` commits, and the live input
screen now lowers through Eve/UI Toolkit instead of the old drag/drop uGUI
shell. Keyboard display layout edits remain runtime-only until Verse owns a
typed layout document.
The unused Unity Localization package and imported Google Sheets localization
sample have been removed; the old transitive Unity Newtonsoft package is no
longer in the manifest or package lock.

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

View file

@ -0,0 +1,19 @@
namespace GameCult.Aetheria.State.Unity
{
public static class AetheriaRuntimeInputSettingsCommands
{
public const string SurfaceId = "aetheria.input_settings";
public const string Refresh = "aetheria.input_settings.refresh";
public const string BeginCapture = "aetheria.input_settings.binding.capture";
public const string CancelCapture = "aetheria.input_settings.binding.cancel";
public const string ToggleActionBar = "aetheria.input_settings.action_bar.toggle";
public static bool IsKnown(string command)
{
return command == Refresh ||
command == BeginCapture ||
command == CancelCapture ||
command == ToggleActionBar;
}
}
}

View file

@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace GameCult.Aetheria.State.Unity
{
public sealed class AetheriaRuntimeInputSettingsSurfaceState
{
public AetheriaRuntimeInputSettingsSurfaceState(
IReadOnlyList<AetheriaRuntimeInputBindingSurfaceState> bindings,
IReadOnlyList<AetheriaRuntimeActionBarInputSurfaceState> actionBarInputs,
bool capturePending,
string capturePrompt,
string updatedAtUtc)
{
Bindings = bindings ?? Array.Empty<AetheriaRuntimeInputBindingSurfaceState>();
ActionBarInputs = actionBarInputs ?? Array.Empty<AetheriaRuntimeActionBarInputSurfaceState>();
CapturePending = capturePending;
CapturePrompt = capturePrompt ?? "";
UpdatedAtUtc = updatedAtUtc ?? "";
}
public IReadOnlyList<AetheriaRuntimeInputBindingSurfaceState> Bindings { get; }
public IReadOnlyList<AetheriaRuntimeActionBarInputSurfaceState> ActionBarInputs { get; }
public bool CapturePending { get; }
public string CapturePrompt { get; }
public string UpdatedAtUtc { get; }
}
public sealed class AetheriaRuntimeInputBindingSurfaceState
{
public AetheriaRuntimeInputBindingSurfaceState(
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; }
}
public sealed class AetheriaRuntimeActionBarInputSurfaceState
{
public AetheriaRuntimeActionBarInputSurfaceState(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 static class AetheriaRuntimeInputSettingsSurfaceBuilder
{
public static AetheriaRuntimeSurfaceDocument Build(
AetheriaRuntimeInputSettingsSurfaceState settings,
long version = 1)
{
settings ??= new AetheriaRuntimeInputSettingsSurfaceState(
Array.Empty<AetheriaRuntimeInputBindingSurfaceState>(),
Array.Empty<AetheriaRuntimeActionBarInputSurfaceState>(),
capturePending: false,
capturePrompt: "",
updatedAtUtc: "");
return new AetheriaRuntimeSurfaceDocument(
providerId: "aetheria",
providerKind: "game.runtime",
title: "Aetheria Input Settings",
version: version,
updatedAtUtc: settings.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",
settings.Bindings.Count.ToString()),
Metric(
"aetheria.inputSettings.summary.actionBar",
"Action-Bar Inputs",
settings.ActionBarInputs.Count(entry => entry.Enabled).ToString()),
Text(
"aetheria.inputSettings.summary.note",
"Low-level InputSystem edits flow through this Eve surface and queue typed player-settings commits.")),
Node(
"aetheria.inputSettings.capture",
"card",
new[] { ("title", "Capture") },
Text(
"aetheria.inputSettings.capture.note",
settings.CapturePending
? settings.CapturePrompt
: "Choose a binding row, then press a keyboard or mouse input to capture it."),
ButtonRow(
"aetheria.inputSettings.capture.actions",
settings.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)>(),
settings.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)>(),
settings.ActionBarInputs.Select(BuildActionBarCard).ToArray())),
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
commands: new[]
{
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeInputSettingsCommands.Refresh,
"Refresh",
"unity-uitoolkit"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeInputSettingsCommands.BeginCapture,
"Capture Binding",
"unity-uitoolkit"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeInputSettingsCommands.CancelCapture,
"Cancel Capture",
"unity-uitoolkit"),
new AetheriaRuntimeSurfaceCommandTemplate(
AetheriaRuntimeInputSettingsCommands.ToggleActionBar,
"Toggle Action-Bar Input",
"unity-uitoolkit")
});
}
private static AetheriaRuntimeSurfaceComponent BuildBindingCard(
AetheriaRuntimeInputBindingSurfaceState 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(
AetheriaRuntimeActionBarInputSurfaceState 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>());
}
}
}

View file

@ -192,7 +192,7 @@ namespace GameCult.Aetheria.State.Unity
("activeRun", settings.ActiveRunKey)),
Text(
"playerSettings.summary.note",
"Input remapping still lives on the runtime-owned screen until Eve grows typed rebinding controls.")),
"Input remapping lowers through the runtime Eve input screen and queues typed player-settings commits.")),
Node(
"aetheria.playerSettings.gameplay",
"card",

View file

@ -92,9 +92,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
remapping UI that calls Unity's InputSystem at the binding/action layer;
Unity's generated
`AetheriaInput` class is the edge consumer of typed binding overrides, not
the durable owner. Binding drag/drop and action-bar remapping both route
through named runtime input commit methods instead of writing the input
collections directly. The
the durable owner. The runtime Eve input screen now owns low-level rebinding
and action-bar input toggles, routing them through named runtime input commit
methods instead of writing the input collections directly. The
action bar also uses typed runtime catalog category rows to reject
non-consumable inventory drops and creates consumable bindings around typed
catalog rows. Missing typed rows are rejected for consumable binding instead
@ -1576,10 +1576,12 @@ First Aetheria surfaces to publish:
single Eve host instead of the old `PropertiesPanel`/fade shell. The menu
remains a client-side projection: the Aetheria daemon owns Verse state and
Unity renders and submits commands. The input page now reports typed state
and delegates to the live runtime remap screen in the in-game overlay
instead of pretending that owner is still future work; typed Eve rebinding
controls are still future work. The fake audio page is deleted until audio
has a typed state owner.
and opens the live runtime Eve input screen in the in-game overlay instead
of the old drag/drop uGUI remap shell. The fake audio page is deleted
until audio has a typed state owner.
- Done: replace the live runtime input remap screen with an Eve-owned UI
Toolkit surface that still calls into Aetheria's low-level InputSystem
remapping layer through typed player-settings commit methods.
- Done: replace the sector-map zone details panel with an Eve-owned UI
Toolkit surface. `SectorRenderer` still owns reveal/camera behavior, but
zone inspection no longer rebuilds `PropertiesPanel` rows when the player
@ -2020,9 +2022,11 @@ First Aetheria surfaces to publish:
owning weapon-group toggles, action-bar group binding, override shutdown,
and thermotoggle controls directly instead of `PropertiesPanel.Inspect(...)`.
- Keyboard display layout parsing is no longer JSON-backed:
`InputDisplayLayout` builds its static ANSI-104 display projection from typed
`InputLayout` rows/columns. The dead commented Ink `ToJson` write path and
checked-in `ansi104.json` display file have been removed from live source.
the runtime input screen builds its action-bar candidates and rebinding UI
from typed input paths, and `InputLayout` remains the typed ANSI-104 keyboard
geometry helper instead of a JSON text asset. The dead commented Ink `ToJson`
write path and checked-in `ansi104.json` display file have been removed from
live source.
- `Aetheria.State.Smoke` proves the provider-owned Eve command bridge drains
`gamecult.eve.command.v1` envelopes, accepts advertised refresh commands plus
player-settings mutation commands, rejects unknown commands, persists