Move input rebinding to Eve UI Toolkit
This commit is contained in:
parent
1bb80638f2
commit
cbbc6fe12d
8 changed files with 743 additions and 479 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue