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