using System; using System.Collections.Generic; 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 readonly Dictionary DisplayNameCache = new(StringComparer.Ordinal); private static readonly Dictionary SpriteTagCache = new(); 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 = GetSpriteTag(sprite); 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; } if (DisplayNameCache.TryGetValue(controlPath, out string cachedDisplayName)) { return cachedDisplayName; } string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice); if (!string.IsNullOrWhiteSpace(humanReadable)) { DisplayNameCache[controlPath] = humanReadable; return humanReadable; } int separatorIndex = controlPath.LastIndexOf('/'); string last = (separatorIndex >= 0 ? controlPath.Substring(separatorIndex + 1) : controlPath).Trim(TrimChars); DisplayNameCache[controlPath] = last; 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); int tokenStart = 0; for (int i = 0; i <= groups.Length; i++) { if (i < groups.Length && groups[i] != InputBinding.Separator) { continue; } int tokenLength = i - tokenStart; while (tokenLength > 0 && char.IsWhiteSpace(groups[tokenStart])) { tokenStart++; tokenLength--; } while (tokenLength > 0 && char.IsWhiteSpace(groups[tokenStart + tokenLength - 1])) { tokenLength--; } if (tokenLength > 0) { string token = groups.Substring(tokenStart, tokenLength); if (ContainsAny(token, hints)) { return true; } } tokenStart = i + 1; } return false; } private static string GetSpriteTag(Sprite sprite) { if (sprite == null) { return null; } int instanceId = sprite.GetInstanceID(); if (SpriteTagCache.TryGetValue(instanceId, out string cachedTag)) { return cachedTag; } cachedTag = $""; SpriteTagCache[instanceId] = cachedTag; return cachedTag; } 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 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 string GetEffectivePath(InputBinding binding) { return string.IsNullOrWhiteSpace(binding.effectivePath) ? binding.path : binding.effectivePath; } 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); } } }