using System; using UnityEngine; using UnityEngine.InputSystem; public static class GlyphService { private static readonly string[] KeyboardGroupHints = { "keyboard", "mouse", "keyboard&mouse", "keyboardmouse", "kbm" }; private static readonly string[] XboxGroupHints = { "xbox", "xinput", "gamepad", "controller" }; private static readonly string[] PlayStationGroupHints = { "playstation", "dualshock", "dualsense", "gamepad", "controller" }; private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" }; private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' }; private static InputGlyphDatabase _database; public static InputGlyphDatabase Database { get { if (_database == null) { _database = Resources.Load("InputGlyphDatabase"); } return _database; } } public static string GetBindingControlPath( InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) { return TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding) ? GetEffectivePath(binding) : string.Empty; } public static string GetBindingControlPath( InputActionReference actionReference, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) { return GetBindingControlPath(actionReference != null ? actionReference.action : null, compositePartName, deviceOverride); } public static bool TryGetTMPTagForActionPath( InputAction action, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) { string controlPath = GetBindingControlPath(action, compositePartName, device); return TryGetTMPTagForActionPath(controlPath, device, out tag, out displayFallback, db); } public static bool TryGetTMPTagForActionPath( InputActionReference actionReference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) { return TryGetTMPTagForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out tag, out displayFallback, db); } public static bool TryGetUISpriteForActionPath( InputAction action, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null) { string controlPath = GetBindingControlPath(action, compositePartName, device); return TryGetUISpriteForActionPath(controlPath, device, out sprite, db); } public static bool TryGetUISpriteForActionPath( InputActionReference actionReference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null) { return TryGetUISpriteForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out sprite, db); } public static bool TryGetTMPTagForActionPath( string controlPath, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) { displayFallback = GetDisplayNameFromControlPath(controlPath); tag = null; if (!TryGetUISpriteForActionPath(controlPath, device, out Sprite sprite, db)) { return false; } tag = $""; return true; } public static bool TryGetUISpriteForActionPath( string controlPath, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null) { sprite = null; db ??= Database; return db != null && db.TryGetSprite(controlPath, device, out sprite); } public static string GetDisplayNameFromInputAction( InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) { if (!TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding)) { return string.Empty; } string display = binding.ToDisplayString(); return string.IsNullOrEmpty(display) ? GetDisplayNameFromControlPath(GetEffectivePath(binding)) : display; } public static string GetDisplayNameFromControlPath(string controlPath) { if (string.IsNullOrWhiteSpace(controlPath)) { return string.Empty; } string[] parts = controlPath.Split('/'); string last = parts[parts.Length - 1].Trim(TrimChars); return last; } public static bool TryGetBindingControl( InputAction action, string compositePartName, InputDeviceWatcher.InputDeviceCategory? deviceOverride, out InputBinding binding) { binding = default; if (action == null) { return false; } InputDeviceWatcher.InputDeviceCategory category = deviceOverride ?? InputDeviceWatcher.CurrentCategory; int bestScore = int.MinValue; bool requireCompositePart = !string.IsNullOrEmpty(compositePartName); for (int i = 0; i < action.bindings.Count; i++) { InputBinding candidate = action.bindings[i]; if (candidate.isComposite) { continue; } if (requireCompositePart) { if (!candidate.isPartOfComposite || !string.Equals(candidate.name, compositePartName, StringComparison.OrdinalIgnoreCase)) { continue; } } else if (candidate.isPartOfComposite) { continue; } string path = GetEffectivePath(candidate); if (string.IsNullOrWhiteSpace(path)) { continue; } int score = ScoreBinding(candidate, category); if (score > bestScore) { bestScore = score; binding = candidate; } } return bestScore > int.MinValue; } private static int ScoreBinding(InputBinding binding, InputDeviceWatcher.InputDeviceCategory category) { int score = 0; string path = GetEffectivePath(binding); if (MatchesBindingGroups(binding.groups, category)) { score += 100; } else if (!string.IsNullOrWhiteSpace(binding.groups)) { score -= 20; } if (MatchesControlPath(path, category)) { score += 60; } if (!binding.isPartOfComposite) { score += 5; } return score; } private static bool MatchesBindingGroups(string groups, InputDeviceWatcher.InputDeviceCategory category) { if (string.IsNullOrWhiteSpace(groups)) { return false; } string[] hints = GetGroupHints(category); string[] tokens = groups.Split(InputBinding.Separator); for (int i = 0; i < tokens.Length; i++) { string token = tokens[i].Trim(); if (ContainsAny(token, hints)) { return true; } } return false; } private static bool MatchesControlPath(string path, InputDeviceWatcher.InputDeviceCategory category) { if (string.IsNullOrWhiteSpace(path)) { return false; } switch (category) { case InputDeviceWatcher.InputDeviceCategory.Keyboard: return StartsWithDevice(path, "") || StartsWithDevice(path, ""); case InputDeviceWatcher.InputDeviceCategory.Xbox: return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, XboxGroupHints); case InputDeviceWatcher.InputDeviceCategory.PlayStation: return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, PlayStationGroupHints); default: return StartsWithDevice(path, "") || StartsWithDevice(path, "") || ContainsAny(path, OtherGamepadGroupHints); } } private static bool StartsWithDevice(string path, string deviceTag) { return path.StartsWith(deviceTag, StringComparison.OrdinalIgnoreCase); } private static string[] GetGroupHints(InputDeviceWatcher.InputDeviceCategory category) { switch (category) { case InputDeviceWatcher.InputDeviceCategory.Keyboard: return KeyboardGroupHints; case InputDeviceWatcher.InputDeviceCategory.Xbox: return XboxGroupHints; case InputDeviceWatcher.InputDeviceCategory.PlayStation: return PlayStationGroupHints; default: return OtherGamepadGroupHints; } } private static bool ContainsAny(string source, string[] hints) { if (string.IsNullOrWhiteSpace(source) || hints == null) { return false; } for (int i = 0; i < hints.Length; i++) { if (source.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } private static string GetEffectivePath(InputBinding binding) { return string.IsNullOrWhiteSpace(binding.effectivePath) ? binding.path : binding.effectivePath; } }