2026-03-20 16:50:30 +08:00
|
|
|
|
using System;
|
2026-04-29 11:04:57 +08:00
|
|
|
|
using Cysharp.Text;
|
2026-03-20 16:50:30 +08:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.InputSystem;
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ? Input System ????? UI Sprite?TMP Sprite Tag ????????
|
|
|
|
|
|
/// ?????????????????????? SetDatabase ???
|
|
|
|
|
|
/// </summary>
|
2026-03-20 16:50:30 +08:00
|
|
|
|
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 = { '{', '}', '<', '>', '\'', '"' };
|
2026-04-29 11:04:57 +08:00
|
|
|
|
private const int InitialCacheCapacity = 64;
|
|
|
|
|
|
private static string[] DisplayNameKeys = new string[InitialCacheCapacity];
|
|
|
|
|
|
private static string[] DisplayNameValues = new string[InitialCacheCapacity];
|
|
|
|
|
|
private static int DisplayNameCount;
|
|
|
|
|
|
private static int[] SpriteTagKeys = new int[InitialCacheCapacity];
|
|
|
|
|
|
private static string[] SpriteTagValues = new string[InitialCacheCapacity];
|
|
|
|
|
|
private static int SpriteTagCount;
|
2026-03-20 16:50:30 +08:00
|
|
|
|
|
|
|
|
|
|
private static InputGlyphDatabase _database;
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ???? Glyph ????????????????????
|
|
|
|
|
|
/// ??????????????? Resources.Load?
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static void SetDatabase(InputGlyphDatabase database)
|
|
|
|
|
|
{
|
|
|
|
|
|
_database = database;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ????????????
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static void ClearDatabase()
|
|
|
|
|
|
{
|
|
|
|
|
|
_database = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 16:12:50 +08:00
|
|
|
|
static InputGlyphDatabase Database
|
2026-03-20 16:50:30 +08:00
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
return _database;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ??????????????????????????
|
|
|
|
|
|
/// </summary>
|
2026-03-20 16:50:30 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ? Action ?????????? TMP Sprite Tag?????????????
|
|
|
|
|
|
/// </summary>
|
2026-03-20 16:50:30 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ? Action ?????????????????? UI Sprite?
|
|
|
|
|
|
/// </summary>
|
2026-03-20 16:50:30 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 将原始控制路径解析为 TMP Sprite Tag,失败时回退为可读控制名。
|
|
|
|
|
|
/// </summary>
|
2026-03-20 16:50:30 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 通过 Glyph 数据库查找表将原始控制路径解析为 UI Sprite。
|
|
|
|
|
|
/// </summary>
|
2026-03-20 16:50:30 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ?? Action ?????????????
|
|
|
|
|
|
/// </summary>
|
2026-03-20 16:50:30 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ?????????????????
|
|
|
|
|
|
/// ??????????????????????????
|
|
|
|
|
|
/// </summary>
|
2026-03-20 16:50:30 +08:00
|
|
|
|
public static string GetDisplayNameFromControlPath(string controlPath)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(controlPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
int cacheIndex = IndexOf(DisplayNameKeys, DisplayNameCount, controlPath);
|
|
|
|
|
|
if (cacheIndex >= 0)
|
2026-03-20 16:50:30 +08:00
|
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
|
return DisplayNameValues[cacheIndex];
|
2026-03-20 16:50:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice);
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(humanReadable))
|
|
|
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
|
AddDisplayNameCache(controlPath, humanReadable);
|
2026-03-20 16:50:30 +08:00
|
|
|
|
return humanReadable;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int separatorIndex = controlPath.LastIndexOf('/');
|
|
|
|
|
|
string last = (separatorIndex >= 0 ? controlPath.Substring(separatorIndex + 1) : controlPath).Trim(TrimChars);
|
2026-04-29 11:04:57 +08:00
|
|
|
|
AddDisplayNameCache(controlPath, last);
|
2026-03-20 16:50:30 +08:00
|
|
|
|
return last;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// ?? binding group ????????????????????
|
|
|
|
|
|
/// </summary>
|
2026-03-20 16:50:30 +08:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
|
if (ContainsAny(groups, tokenStart, tokenLength, hints))
|
2026-03-20 16:50:30 +08:00
|
|
|
|
{
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokenStart = i + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string GetSpriteTag(Sprite sprite)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (sprite == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int instanceId = sprite.GetInstanceID();
|
2026-04-29 11:04:57 +08:00
|
|
|
|
int cacheIndex = IndexOf(SpriteTagKeys, SpriteTagCount, instanceId);
|
|
|
|
|
|
if (cacheIndex >= 0)
|
2026-03-20 16:50:30 +08:00
|
|
|
|
{
|
2026-04-29 11:04:57 +08:00
|
|
|
|
return SpriteTagValues[cacheIndex];
|
2026-03-20 16:50:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
string cachedTag = ZString.Concat("<sprite name=\"", sprite.name, "\">");
|
|
|
|
|
|
AddSpriteTagCache(instanceId, cachedTag);
|
2026-03-20 16:50:30 +08:00
|
|
|
|
return cachedTag;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-29 11:04:57 +08:00
|
|
|
|
private static void AddDisplayNameCache(string key, string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (DisplayNameCount == DisplayNameKeys.Length)
|
|
|
|
|
|
{
|
|
|
|
|
|
Array.Resize(ref DisplayNameKeys, DisplayNameKeys.Length << 1);
|
|
|
|
|
|
Array.Resize(ref DisplayNameValues, DisplayNameValues.Length << 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DisplayNameKeys[DisplayNameCount] = key;
|
|
|
|
|
|
DisplayNameValues[DisplayNameCount] = value;
|
|
|
|
|
|
DisplayNameCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void AddSpriteTagCache(int key, string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (SpriteTagCount == SpriteTagKeys.Length)
|
|
|
|
|
|
{
|
|
|
|
|
|
Array.Resize(ref SpriteTagKeys, SpriteTagKeys.Length << 1);
|
|
|
|
|
|
Array.Resize(ref SpriteTagValues, SpriteTagValues.Length << 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
SpriteTagKeys[SpriteTagCount] = key;
|
|
|
|
|
|
SpriteTagValues[SpriteTagCount] = value;
|
|
|
|
|
|
SpriteTagCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int IndexOf(string[] values, int count, string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.Equals(values[i], value, StringComparison.Ordinal))
|
|
|
|
|
|
{
|
|
|
|
|
|
return i;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int IndexOf(int[] values, int count, int value)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (values[i] == value)
|
|
|
|
|
|
{
|
|
|
|
|
|
return i;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool ContainsAny(string source, int startIndex, int length, string[] hints)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(source) || hints == null || length <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < hints.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (IndexOfIgnoreCase(source, startIndex, length, hints[i]) >= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int IndexOfIgnoreCase(string source, int startIndex, int length, string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(value) || value.Length > length)
|
|
|
|
|
|
{
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int end = startIndex + length - value.Length;
|
|
|
|
|
|
for (int i = startIndex; i <= end; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int valueIndex = 0;
|
|
|
|
|
|
while (valueIndex < value.Length && char.ToUpperInvariant(source[i + valueIndex]) == char.ToUpperInvariant(value[valueIndex]))
|
|
|
|
|
|
{
|
|
|
|
|
|
valueIndex++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (valueIndex == value.Length)
|
|
|
|
|
|
{
|
|
|
|
|
|
return i;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 16:50:30 +08:00
|
|
|
|
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, "<Keyboard>") || StartsWithDevice(path, "<Mouse>");
|
|
|
|
|
|
case InputDeviceWatcher.InputDeviceCategory.Xbox:
|
|
|
|
|
|
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, XboxGroupHints);
|
|
|
|
|
|
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
|
|
|
|
|
|
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, PlayStationGroupHints);
|
|
|
|
|
|
default:
|
|
|
|
|
|
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, OtherGamepadGroupHints);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|