using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Utilities; namespace InputGlyphsFramework { /// /// Handles interactive rebinding and conflict resolution for keyboard mappings. /// Conflicts are detected within configured scopes (e.g. Gameplay vs UI) and the conflicting binding(s) /// will be cleared (erased) while keeping the new mapping. /// [DefaultExecutionOrder(-150)] public class RebindManager : MonoBehaviour { public static RebindManager Instance { get; private set; } [Tooltip("Action map names treated as Gameplay scope (conflicts resolved inside this group)")] public string[] gameplayMapNames = new string[] { "Player", "Other" }; [Tooltip("Action map names treated as UI scope")] public string[] uiMapNames = new string[] { "UI" }; [Header("Rebind Options")] public bool excludeMousePosition = true; public string cancelControl = "/escape"; private void Awake() { if (Instance != null && Instance != this) { Destroy(this); return; } Instance = this; } private void OnDestroy() { if (Instance == this) Instance = null; } /// /// Start interactive rebind on given action & bindingIndex. scope chooses which maps to treat as same-scope /// for conflict detection. Use scope = "Gameplay" or "UI". /// public void StartInteractiveRebind(InputActionReference actionRef, int bindingIndex, string scope = "Gameplay", Action onComplete = null) { if (actionRef == null || actionRef.action == null) return; var action = actionRef.action; if (bindingIndex < 0 || bindingIndex >= action.bindings.Count) { Debug.LogWarning("RebindManager: bindingIndex out of range"); return; } var rebind = action.PerformInteractiveRebinding(bindingIndex) .WithCancelingThrough(cancelControl) .OnComplete(op => { try { var newPath = action.bindings[bindingIndex].effectivePath; // may be set via override // Persist override for this binding action.ApplyBindingOverride(bindingIndex, newPath); // Resolve conflicts inside scope (keyboard only) ResolveConflicts(action, bindingIndex, newPath, scope); // Save using InputService if (InputService.Instance != null) InputService.Instance.SaveOverridesToPrefs(); // Notify if (InputService.Instance != null) InputService.Instance.NotifyBindingChanged(action, bindingIndex); onComplete?.Invoke(); } finally { op.Dispose(); } }); if (excludeMousePosition) rebind.WithControlsExcluding("/position"); rebind.Start(); } private IEnumerable GetMapsForScope(string scope) { if (string.Equals(scope, "UI", StringComparison.OrdinalIgnoreCase)) return uiMapNames; return gameplayMapNames; } private void ResolveConflicts(InputAction changedAction, int changedBindingIndex, string newPath, string scope) { if (string.IsNullOrEmpty(newPath)) return; // only care about keyboard rebinding conflicts if (!newPath.ToLowerInvariant().Contains("keyboard")) return; var maps = GetMapsForScope(scope); if (maps == null || maps.Count() == 0) return; if (InputService.Instance == null || InputService.Instance.playerInput == null) return; var asset = InputService.Instance.playerInput.actions; var actionsToCheck = new List<(InputAction action, int bindingIndex)>(); // Gather candidates: all bindings in maps matching scope foreach (var map in asset.actionMaps) { if (!maps.Contains(map.name)) continue; foreach (var action in map.actions) { for (int i = 0; i < action.bindings.Count; ++i) { var b = action.bindings[i]; // ignore the binding we just changed on the changedAction if (action == changedAction && i == changedBindingIndex) continue; // consider only keyboard bindings (or empty path) for conflict var path = !string.IsNullOrEmpty(b.effectivePath) ? b.effectivePath : b.path; if (string.IsNullOrEmpty(path)) continue; if (path.Equals(newPath, StringComparison.OrdinalIgnoreCase)) { actionsToCheck.Add((action, i)); } } } } if (actionsToCheck.Count == 0) return; // Log conflicts, then clear conflicting bindings (erase) foreach (var item in actionsToCheck) { Debug.LogWarning($"Rebind conflict detected: New binding '{newPath}' for '{changedAction.name}' conflicts with '{item.action.name}' binding index {item.bindingIndex} (will be cleared)"); try { // Erase the conflicting binding entry item.action.ChangeBinding(item.bindingIndex).Erase(); // Notify for the cleared action if (InputService.Instance != null) InputService.Instance.NotifyBindingChanged(item.action, item.bindingIndex); } catch (Exception e) { Debug.LogWarning("RebindManager: Failed to erase conflicting binding: " + e.Message); } } } } }