226 lines
10 KiB
C#
226 lines
10 KiB
C#
using System;
|
|||
|
|
using System.Collections.Generic;
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.InputSystem;
|
||
|
|
|
||
|
|
namespace Ironspore
|
||
|
|
{
|
||
|
|
public sealed class BindingSlot
|
||
|
|
{
|
||
|
|
public readonly InputAction Action;
|
||
|
|
public readonly int Index;
|
||
|
|
public readonly string LabelKey;
|
||
|
|
public BindingSlot(InputAction action, int index)
|
||
|
|
{
|
||
|
|
Action = action;
|
||
|
|
Index = index;
|
||
|
|
LabelKey = "settings.action." + action.name.ToLowerInvariant()
|
||
|
|
+ (action.bindings[index].isPartOfComposite ? "." + action.bindings[index].name.ToLowerInvariant() : "");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public enum BindingChange { Success, Cancelled, Conflict, Reserved, Unsupported }
|
||
|
|
|
||
|
|
public sealed class InputBindingService : IDisposable
|
||
|
|
{
|
||
|
|
readonly InputActionAsset input;
|
||
|
|
readonly List<BindingSlot> slots = new List<BindingSlot>();
|
||
|
|
readonly List<InputAction> enabledActions = new List<InputAction>();
|
||
|
|
InputActionRebindingExtensions.RebindingOperation operation;
|
||
|
|
BindingSlot activeSlot;
|
||
|
|
Action<BindingChange, BindingSlot> onCompleted;
|
||
|
|
public IReadOnlyList<BindingSlot> Slots => slots;
|
||
|
|
public bool IsRebinding => operation != null;
|
||
|
|
public int LastRebindFrame { get; private set; } = -1;
|
||
|
|
// Input callbacks can run before Unity advances frameCount for Update.
|
||
|
|
public bool SuppressMenuInput => IsRebinding || (LastRebindFrame >= 0 && Time.frameCount <= LastRebindFrame + 1);
|
||
|
|
|
||
|
|
public InputBindingService(InputActionAsset input)
|
||
|
|
{
|
||
|
|
this.input = input;
|
||
|
|
foreach (var action in input.FindActionMap("Pilot", true).actions)
|
||
|
|
for (int i = 0; i < action.bindings.Count; i++)
|
||
|
|
if (action.name != "Look" && !action.bindings[i].isComposite)
|
||
|
|
slots.Add(new BindingSlot(action, i));
|
||
|
|
}
|
||
|
|
|
||
|
|
public string Save() => input.SaveBindingOverridesAsJson();
|
||
|
|
|
||
|
|
public bool TryLoad(string json)
|
||
|
|
{
|
||
|
|
Cancel();
|
||
|
|
string previous = Save();
|
||
|
|
try
|
||
|
|
{
|
||
|
|
input.RemoveAllBindingOverrides();
|
||
|
|
if (!string.IsNullOrEmpty(json))
|
||
|
|
input.LoadBindingOverridesFromJson(json);
|
||
|
|
// Older profiles may already use R. Preserve their binding and move Reload.
|
||
|
|
var reload = slots.Find(slot => slot.Action.name == "Reload");
|
||
|
|
if (reload != null && !reload.Action.bindings[reload.Index].hasOverrides &&
|
||
|
|
Check(reload, reload.Action.bindings[reload.Index].path, out _) == BindingChange.Conflict)
|
||
|
|
{
|
||
|
|
foreach (char letter in "tyuiohjklbnmpxczaqwsdefgvr")
|
||
|
|
if (Rebind(reload, "<Keyboard>/" + letter, out _) == BindingChange.Success)
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
foreach (var map in input.actionMaps)
|
||
|
|
foreach (var action in map.actions)
|
||
|
|
foreach (var binding in action.bindings)
|
||
|
|
if (binding.hasOverrides && (action.name == "Look" || binding.isComposite
|
||
|
|
|| binding.overrideInteractions != null || binding.overrideProcessors != null))
|
||
|
|
throw new ArgumentException("Only button binding paths can be overridden.");
|
||
|
|
foreach (var slot in slots)
|
||
|
|
if (slot.Action.bindings[slot.Index].hasOverrides && Check(slot, slot.Action.bindings[slot.Index].effectivePath, out _) != BindingChange.Success)
|
||
|
|
throw new ArgumentException("Invalid or conflicting binding override.");
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
catch (Exception)
|
||
|
|
{
|
||
|
|
input.RemoveAllBindingOverrides();
|
||
|
|
input.LoadBindingOverridesFromJson(previous);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Reset() { Cancel(); input.RemoveAllBindingOverrides(); }
|
||
|
|
|
||
|
|
public BindingChange Check(BindingSlot target, string path, out BindingSlot conflict)
|
||
|
|
{
|
||
|
|
conflict = null;
|
||
|
|
if (string.Equals(path, "<Keyboard>/escape", StringComparison.OrdinalIgnoreCase) && target.Action.name != "Pause")
|
||
|
|
return BindingChange.Reserved;
|
||
|
|
if (!Supported(path))
|
||
|
|
return BindingChange.Unsupported;
|
||
|
|
foreach (var slot in slots)
|
||
|
|
if ((slot.Action != target.Action || slot.Index != target.Index)
|
||
|
|
&& string.Equals(slot.Action.bindings[slot.Index].effectivePath, path, StringComparison.OrdinalIgnoreCase))
|
||
|
|
{
|
||
|
|
conflict = slot;
|
||
|
|
return BindingChange.Conflict;
|
||
|
|
}
|
||
|
|
return BindingChange.Success;
|
||
|
|
}
|
||
|
|
|
||
|
|
public BindingChange Rebind(BindingSlot slot, string path, out BindingSlot conflict)
|
||
|
|
{
|
||
|
|
var result = Check(slot, path, out conflict);
|
||
|
|
if (result == BindingChange.Success)
|
||
|
|
slot.Action.ApplyBindingOverride(slot.Index, path);
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Begin(BindingSlot slot, Action<BindingChange, BindingSlot> completed)
|
||
|
|
{
|
||
|
|
Cancel();
|
||
|
|
activeSlot = slot;
|
||
|
|
onCompleted = completed;
|
||
|
|
enabledActions.Clear();
|
||
|
|
foreach (var map in input.actionMaps)
|
||
|
|
foreach (var action in map.actions)
|
||
|
|
if (action.enabled)
|
||
|
|
enabledActions.Add(action);
|
||
|
|
input.Disable();
|
||
|
|
BindingChange result = BindingChange.Cancelled;
|
||
|
|
BindingSlot conflict = null;
|
||
|
|
operation = slot.Action.PerformInteractiveRebinding(slot.Index)
|
||
|
|
.WithExpectedControlType("Button")
|
||
|
|
.WithControlsHavingToMatchPath("<Keyboard>")
|
||
|
|
.WithControlsHavingToMatchPath("<Mouse>")
|
||
|
|
.WithControlsExcluding("<Mouse>/position")
|
||
|
|
.WithControlsExcluding("<Mouse>/delta")
|
||
|
|
.WithControlsExcluding("<Mouse>/scroll")
|
||
|
|
// Let the UI adapter distinguish a cancel-button click from a left-button binding.
|
||
|
|
.WithControlsExcluding("<Mouse>/leftButton")
|
||
|
|
.WithControlsExcluding("<Keyboard>/anyKey")
|
||
|
|
.WithControlsExcluding("<Keyboard>/shift")
|
||
|
|
.WithControlsExcluding("<Keyboard>/ctrl")
|
||
|
|
.WithControlsExcluding("<Keyboard>/alt")
|
||
|
|
.WithCancelingThrough("<Keyboard>/escape")
|
||
|
|
.OnMatchWaitForAnother(0)
|
||
|
|
.OnApplyBinding((_, path) => result = Rebind(slot, path, out conflict))
|
||
|
|
.OnCancel(_ => Finish(BindingChange.Cancelled, null))
|
||
|
|
.OnComplete(_ => Finish(result, conflict));
|
||
|
|
operation.Start();
|
||
|
|
}
|
||
|
|
|
||
|
|
// UI Toolkit also delivers keyboard/pointer events when a platform does not forward
|
||
|
|
// synthetic or accessibility input as raw device events. Both routes use the same validator.
|
||
|
|
public bool CapturePath(string path)
|
||
|
|
{
|
||
|
|
if (!IsRebinding)
|
||
|
|
return false;
|
||
|
|
var completed = onCompleted;
|
||
|
|
onCompleted = null;
|
||
|
|
var result = Rebind(activeSlot, path, out var conflict);
|
||
|
|
Cancel();
|
||
|
|
completed?.Invoke(result, conflict);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void Finish(BindingChange result, BindingSlot conflict)
|
||
|
|
{
|
||
|
|
var finished = operation;
|
||
|
|
var completed = onCompleted;
|
||
|
|
operation = null;
|
||
|
|
onCompleted = null;
|
||
|
|
activeSlot = null;
|
||
|
|
finished?.Dispose();
|
||
|
|
foreach (var action in enabledActions)
|
||
|
|
action.Enable();
|
||
|
|
enabledActions.Clear();
|
||
|
|
LastRebindFrame = Time.frameCount;
|
||
|
|
completed?.Invoke(result, conflict);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Cancel() => operation?.Cancel();
|
||
|
|
public void Dispose() => Cancel();
|
||
|
|
|
||
|
|
static bool Supported(string path)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(path))
|
||
|
|
return false;
|
||
|
|
if (path.StartsWith("<Keyboard>/", StringComparison.OrdinalIgnoreCase))
|
||
|
|
{
|
||
|
|
string name = path.Substring(11);
|
||
|
|
foreach (var keyControl in InputSystem.LoadLayout("Keyboard").controls)
|
||
|
|
if (keyControl.layout.ToString() == "Key" && string.Equals(keyControl.name.ToString(), name, StringComparison.OrdinalIgnoreCase))
|
||
|
|
return true;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (!path.StartsWith("<Mouse>/", StringComparison.OrdinalIgnoreCase))
|
||
|
|
return false;
|
||
|
|
string control = path.Substring(8).ToLowerInvariant();
|
||
|
|
return control == "leftbutton" || control == "rightbutton" || control == "middlebutton"
|
||
|
|
|| control == "forwardbutton" || control == "backbutton";
|
||
|
|
}
|
||
|
|
|
||
|
|
public string Display(BindingSlot slot)
|
||
|
|
{
|
||
|
|
string path = slot.Action.bindings[slot.Index].effectivePath;
|
||
|
|
string special = path?.ToLowerInvariant();
|
||
|
|
switch (special)
|
||
|
|
{
|
||
|
|
case "<mouse>/leftbutton": return GameLocalization.Text("settings.key.mouseLeft");
|
||
|
|
case "<mouse>/rightbutton": return GameLocalization.Text("settings.key.mouseRight");
|
||
|
|
case "<mouse>/middlebutton": return GameLocalization.Text("settings.key.mouseMiddle");
|
||
|
|
case "<mouse>/forwardbutton": return GameLocalization.Text("settings.key.mouseForward");
|
||
|
|
case "<mouse>/backbutton": return GameLocalization.Text("settings.key.mouseBack");
|
||
|
|
case "<keyboard>/space": return GameLocalization.Text("settings.key.space");
|
||
|
|
case "<keyboard>/leftshift": return GameLocalization.Text("settings.key.leftShift");
|
||
|
|
case "<keyboard>/rightshift": return GameLocalization.Text("settings.key.rightShift");
|
||
|
|
}
|
||
|
|
return slot.Action.GetBindingDisplayString(slot.Index);
|
||
|
|
}
|
||
|
|
|
||
|
|
public string Display(string actionName)
|
||
|
|
{
|
||
|
|
var names = new List<string>();
|
||
|
|
foreach (var slot in slots)
|
||
|
|
if (slot.Action.name == actionName)
|
||
|
|
names.Add(Display(slot));
|
||
|
|
return string.Join(" / ", names);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|