From cbbc6fe12d7027f84fd003f6faaf9e8e034eae72 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 17 Jun 2026 08:41:06 +0100 Subject: [PATCH] Move input rebinding to Eve UI Toolkit --- Aetheria.State.Verify/Program.cs | 112 +++ Aetheria.State/README.md | 6 +- .../UI/InputScreen/InputDisplayLayout.cs | 951 ++++++++---------- Assets/Scripts/UI/MainMenu.cs | 8 +- .../AetheriaRuntimeInputSettingsCommands.cs | 19 + ...heriaRuntimeInputSettingsSurfaceBuilder.cs | 262 +++++ ...eriaRuntimePlayerSettingsSurfaceBuilder.cs | 2 +- docs/aetheria-perfect-machine-map.md | 24 +- 8 files changed, 824 insertions(+), 560 deletions(-) create mode 100644 Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeInputSettingsCommands.cs create mode 100644 Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeInputSettingsSurfaceBuilder.cs diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index e6e4a0b78..5e10c8a55 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -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"); diff --git a/Aetheria.State/README.md b/Aetheria.State/README.md index a447842cc..622f5d03e 100644 --- a/Aetheria.State/README.md +++ b/Aetheria.State/README.md @@ -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. diff --git a/Assets/Scripts/UI/InputScreen/InputDisplayLayout.cs b/Assets/Scripts/UI/InputScreen/InputDisplayLayout.cs index 0ee2c0891..43ff44df9 100644 --- a/Assets/Scripts/UI/InputScreen/InputDisplayLayout.cs +++ b/Assets/Scripts/UI/InputScreen/InputDisplayLayout.cs @@ -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().ScreenSpaceRect(); - } + "/leftButton", + "/rightButton", + "/middleButton", + "/forwardButton", + "/backButton", + "/1", + "/2", + "/3", + "/4", + "/5", + "/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 ButtonMappings = new List(); - public Rect LabelRect => Label.GetComponent().ScreenSpaceRect(); - } - - private Dictionary _bindButtons = new Dictionary(); - private List _buttonMappings = new List(); - private List _actionMappings = new List(); - - void Start() - { - _canvas = transform.root.GetComponent(); - _inputLayout = InputLayout.CreateAnsi104(); - DisplayLayout(_inputLayout); - - _buttonMappings.Add(MapMouseButton(MouseLeft, "/leftButton")); - _buttonMappings.Add(MapMouseButton(MouseRight, "/rightButton")); - _buttonMappings.Add(MapMouseButton(MouseMiddle, "/middleButton")); - _buttonMappings.Add(MapMouseButton(MouseForward, "/forwardButton")); - _buttonMappings.Add(MapMouseButton(MouseBack, "/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(); - 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(), 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(); - buttonMapping.LabelLine.color = buttonMapping.ActionMapping.Label.Label.color; - buttonMapping.LabelLine.Points = new[] {keyPoint, labelPoint}; - } - else - { - buttonMapping.LabelLine?.GetComponent().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(); - foreach (var column in keyRow.Columns) - { - if (column is InputLayoutColumnSpacer) - { - displayRow.KeySpacerPrototype.Instantiate().preferredWidth = KeySize * column.Width; - } - else if (column is InputLayoutKey) - { - var displayKey = displayRow.KeyPrototype.Instantiate(); - 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().preferredHeight = KeySize * rowSpacer.Height; - } - else - { - throw new ArgumentOutOfRangeException(nameof(row)); - } - } - } - - private IEnumerator AssociateInputKeys() - { - var path = ""; - var keyPress = new InputAction(binding: "//