using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using UnityEngine.InputSystem.Utilities; namespace UnityEngine.InputSystem.Layouts { /// /// Specification that can be matched against an . This is /// used to find which to create for a device when it is discovered. /// /// /// Each matcher is basically a set of key/value pairs where each value may either be /// a regular expression or a plain value object. The central method for testing a given matcher /// against an is . /// /// Various helper methods such as or /// assist with creating matchers. /// /// /// /// // A matcher that matches a PS4 controller by name. /// new InputDeviceMatcher() /// .WithInterface("HID") /// .WithManufacturer("Sony.+Entertainment") // Regular expression /// .WithProduct("Wireless Controller")); /// /// // A matcher that matches the same controller by PID and VID. /// new InputDeviceMatcher() /// .WithInterface("HID") /// .WithCapability("vendorId", 0x54C) // Sony Entertainment. /// .WithCapability("productId", 0x9CC)); // Wireless controller. /// /// /// /// For each registered in the system that represents /// a device, arbitrary many matchers can be added. A matcher can be supplied either /// at registration time or at any point after using . /// /// /// /// // Supply a matcher at registration time. /// InputSystem.RegisterLayout<DualShock4GamepadHID>( /// matches: new InputDeviceMatcher() /// .WithInterface("HID") /// .WithCapability("vendorId", 0x54C) // Sony Entertainment. /// .WithCapability("productId", 0x9CC)); // Wireless controller. /// /// // Supply a matcher for an already registered layout. /// // This can be called repeatedly and will add another matcher /// // each time. /// InputSystem.RegisterLayoutMatcher<DualShock4GamepadHID>( /// matches: new InputDeviceMatcher() /// .WithInterface("HID") /// .WithManufacturer("Sony.+Entertainment") /// .WithProduct("Wireless Controller")); /// /// /// /// /// /// public struct InputDeviceMatcher : IEquatable { private KeyValuePair[] m_Patterns; /// /// If true, the matcher has been default-initialized and contains no /// matching . /// /// Whether the matcher contains any matching patterns. /// public bool empty => m_Patterns == null; /// /// The list of patterns to match. /// /// List of matching patterns. /// /// Each pattern is comprised of a key and a value. The key determines which part /// of an to match. /// /// The value represents the expected value. This can be either a plain string /// (matched case-insensitive) or a regular expression. /// /// /// /// /// /// /// public IEnumerable> patterns { get { if (m_Patterns == null) yield break; var count = m_Patterns.Length; for (var i = 0; i < count; ++i) yield return new KeyValuePair(m_Patterns[i].Key.ToString(), m_Patterns[i].Value); } } /// /// Add a pattern to to match an . /// /// String to match. /// If true (default), can be /// a regular expression. /// The modified device matcher with the added pattern. /// public InputDeviceMatcher WithInterface(string pattern, bool supportRegex = true) { return With(kInterfaceKey, pattern, supportRegex); } /// /// Add a pattern to to match a . /// /// String to match. /// If true (default), can be /// a regular expression. /// The modified device matcher with the added pattern. /// public InputDeviceMatcher WithDeviceClass(string pattern, bool supportRegex = true) { return With(kDeviceClassKey, pattern, supportRegex); } /// /// Add a pattern to to match a . /// /// String to match. /// If true (default), can be /// a regular expression. /// The modified device matcher with the added pattern. /// public InputDeviceMatcher WithManufacturer(string pattern, bool supportRegex = true) { return With(kManufacturerKey, pattern, supportRegex); } /// /// Add a pattern to to match a . /// /// String to match. /// If true (default), can be /// a regular expression. /// The modified device matcher with the added pattern. /// public InputDeviceMatcher WithProduct(string pattern, bool supportRegex = true) { return With(kProductKey, pattern, supportRegex); } /// /// Add a pattern to to match a . /// /// String to match. /// If true (default), can be /// a regular expression. /// The modified device matcher with the added pattern. /// public InputDeviceMatcher WithVersion(string pattern, bool supportRegex = true) { return With(kVersionKey, pattern, supportRegex); } /// /// Add a pattern to to match an individual capability in . /// /// Path to the JSON property using '/' as a separator, /// e.g. "elements/count". /// Value to match. This can be a string, a regular expression, /// a boolean, an integer, or a float. Floating-point numbers are matched with respect /// for Mathf.Epsilon. Values are converted between types automatically as /// needed (meaning that a bool can be compared to a string, for example). /// Type of value to match. /// The modified device matcher with the added pattern. /// /// Capabilities are stored as JSON strings in . /// A matcher has the ability to match specific properties from the JSON object /// contained in the capabilities string. /// /// /// /// // The description for a HID will usually have a HIDDeviceDescriptor in /// // JSON format found on its InputDeviceDescription.capabilities. So, a /// // real-world device description could look the equivalent of this: /// var description = new InputDeviceDescription /// { /// interfaceName = "HID", /// capabilities = new HID.HIDDeviceDescriptor /// { /// vendorId = 0x54C, /// productId = 0x9CC /// }.ToJson() /// }; /// /// // We can create a device matcher that looks for those to properties /// // directly in the JSON object. /// new InputDeviceMatcher() /// .WithCapability("vendorId", 0x54C) /// .WithCapability("productId", 0x9CC); /// /// /// /// Properties in nested objects can be referenced by separating properties /// with / and properties in arrays can be indexed with [..]. /// /// public InputDeviceMatcher WithCapability(string path, TValue value) { return With(new InternedString(path), value); } private InputDeviceMatcher With(InternedString key, object value, bool supportRegex = true) { // If it's a string, check whether it's a regex. if (supportRegex && value is string str) { var mayBeRegex = !str.All(ch => char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch)) && !double.TryParse(str, out var _); // Avoid '.' in floats forcing the value to be a regex. if (mayBeRegex) value = new Regex(str, RegexOptions.IgnoreCase); } // Add to list. var result = this; ArrayHelpers.Append(ref result.m_Patterns, new KeyValuePair(key, value)); return result; } /// /// Return the level of matching to the given . /// /// A device description. /// A score usually in the range between 0 and 1. /// /// The algorithm computes a score of how well the matcher matches the given description. /// Essentially, a matcher that matches every single property that is present (as in /// not null and not an empty string) in receives /// a score of 1, a matcher that matches none a score of 0. Matches that match only a subset /// receive a score in-between. /// /// An exception to this are capabilities. Every single match of a capability is counted /// as one property match and added to the score. This means that matchers that match /// on multiple capabilities may actually achieve a score >1. /// /// /// /// var description = new InputDeviceDescription /// { /// interfaceName = "HID", /// product = "MadeUpDevice", /// capabilities = new HID.HIDDeviceDescriptor /// { /// vendorId = 0xABC, /// productId = 0xDEF /// }.ToJson() /// }; /// /// // This matcher will achieve a score of 0.666 (2/3) as it /// // matches two out of three available properties. /// new InputDeviceMatcher() /// .WithInterface("HID") /// .WithProduct("MadeUpDevice"); /// /// // This matcher will achieve a score of 1 despite not matching /// // 'product'. The reason is that it matches two keys in /// // 'capabilities'. /// new InputDeviceMatcher() /// .WithInterface("HID") /// .WithCapability("vendorId", 0xABC) /// .WithCapability("productId", 0xDEF); /// /// /// public float MatchPercentage(InputDeviceDescription deviceDescription) { if (empty) return 0; // Go through all patterns. Score is 0 if any of the patterns // doesn't match. var numPatterns = m_Patterns.Length; for (var i = 0; i < numPatterns; ++i) { var key = m_Patterns[i].Key; var pattern = m_Patterns[i].Value; if (key == kInterfaceKey) { if (string.IsNullOrEmpty(deviceDescription.interfaceName) || !MatchSingleProperty(pattern, deviceDescription.interfaceName)) return 0; } else if (key == kDeviceClassKey) { if (string.IsNullOrEmpty(deviceDescription.deviceClass) || !MatchSingleProperty(pattern, deviceDescription.deviceClass)) return 0; } else if (key == kManufacturerKey) { if (string.IsNullOrEmpty(deviceDescription.manufacturer) || !MatchSingleProperty(pattern, deviceDescription.manufacturer)) return 0; } else if (key == kProductKey) { if (string.IsNullOrEmpty(deviceDescription.product) || !MatchSingleProperty(pattern, deviceDescription.product)) return 0; } else if (key == kVersionKey) { if (string.IsNullOrEmpty(deviceDescription.version) || !MatchSingleProperty(pattern, deviceDescription.version)) return 0; } else { // Capabilities match. Take the key as a path into the JSON // object and match the value found at the given path. if (string.IsNullOrEmpty(deviceDescription.capabilities)) return 0; var graph = new JsonParser(deviceDescription.capabilities); if (!graph.NavigateToProperty(key.ToString()) || !graph.CurrentPropertyHasValueEqualTo(new JsonParser.JsonValue { type = JsonParser.JsonValueType.Any, anyValue = pattern})) return 0; } } // All patterns matched. Our score is determined by the number of properties // we matched against. var propertyCountInDescription = GetNumPropertiesIn(deviceDescription); var scorePerProperty = 1.0f / propertyCountInDescription; return numPatterns * scorePerProperty; } private static bool MatchSingleProperty(object pattern, string value) { // String match. if (pattern is string str) return string.Compare(str, value, StringComparison.InvariantCultureIgnoreCase) == 0; // Regex match. if (pattern is Regex regex) return regex.IsMatch(value); return false; } private static int GetNumPropertiesIn(InputDeviceDescription description) { var count = 0; if (!string.IsNullOrEmpty(description.interfaceName)) count += 1; if (!string.IsNullOrEmpty(description.deviceClass)) count += 1; if (!string.IsNullOrEmpty(description.manufacturer)) count += 1; if (!string.IsNullOrEmpty(description.product)) count += 1; if (!string.IsNullOrEmpty(description.version)) count += 1; if (!string.IsNullOrEmpty(description.capabilities)) count += 1; return count; } /// /// Produce a matcher that matches the given device description verbatim. /// /// A device description. /// A matcher that matches exactly. /// /// This method can be used to produce a matcher for an existing device description, /// e.g. when writing a layout that produces /// layouts for devices on the fly. /// public static InputDeviceMatcher FromDeviceDescription(InputDeviceDescription deviceDescription) { var matcher = new InputDeviceMatcher(); if (!string.IsNullOrEmpty(deviceDescription.interfaceName)) matcher = matcher.WithInterface(deviceDescription.interfaceName, false); if (!string.IsNullOrEmpty(deviceDescription.deviceClass)) matcher = matcher.WithDeviceClass(deviceDescription.deviceClass, false); if (!string.IsNullOrEmpty(deviceDescription.manufacturer)) matcher = matcher.WithManufacturer(deviceDescription.manufacturer, false); if (!string.IsNullOrEmpty(deviceDescription.product)) matcher = matcher.WithProduct(deviceDescription.product, false); if (!string.IsNullOrEmpty(deviceDescription.version)) matcher = matcher.WithVersion(deviceDescription.version, false); // We don't include capabilities in this conversion. return matcher; } /// /// Return a string representation useful for debugging. Lists the /// contained in the matcher. /// /// A string representation of the matcher. public override string ToString() { if (empty) return ""; var result = string.Empty; foreach (var pattern in m_Patterns) { if (result.Length > 0) result += $",{pattern.Key}={pattern.Value}"; else result += $"{pattern.Key}={pattern.Value}"; } return result; } /// /// Test whether this matcher is equivalent to the matcher. /// /// Another device matcher. /// True if the two matchers are equivalent. /// /// Two matchers are equivalent if they contain the same number of patterns and the /// same pattern occurs in each of the matchers. Order of the patterns does not /// matter. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "False positive.")] public bool Equals(InputDeviceMatcher other) { if (m_Patterns == other.m_Patterns) return true; if (m_Patterns == null || other.m_Patterns == null) return false; if (m_Patterns.Length != other.m_Patterns.Length) return false; // Pattern count matches. Compare pattern by pattern. Order of patterns doesn't matter. for (var i = 0; i < m_Patterns.Length; ++i) { var thisPattern = m_Patterns[i]; var foundPattern = false; for (var n = 0; n < m_Patterns.Length; ++n) { var otherPattern = other.m_Patterns[n]; if (thisPattern.Key != otherPattern.Key) continue; if (!thisPattern.Value.Equals(otherPattern.Value)) return false; foundPattern = true; break; } if (!foundPattern) return false; } return true; } /// /// Compare this matcher to another. /// /// A matcher object or null. /// True if the matcher is equivalent. /// public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; return obj is InputDeviceMatcher matcher && Equals(matcher); } /// /// Compare two matchers for equivalence. /// /// First device matcher. /// Second device matcher. /// True if the two matchers are equivalent. /// public static bool operator==(InputDeviceMatcher left, InputDeviceMatcher right) { return left.Equals(right); } /// /// Compare two matchers for non-equivalence. /// /// First device matcher. /// Second device matcher. /// True if the two matchers are not equivalent. /// public static bool operator!=(InputDeviceMatcher left, InputDeviceMatcher right) { return !(left == right); } /// /// Compute a hash code for the device matcher. /// /// A hash code for the matcher. public override int GetHashCode() { return m_Patterns != null ? m_Patterns.GetHashCode() : 0; } private static readonly InternedString kInterfaceKey = new InternedString("interface"); private static readonly InternedString kDeviceClassKey = new InternedString("deviceClass"); private static readonly InternedString kManufacturerKey = new InternedString("manufacturer"); private static readonly InternedString kProductKey = new InternedString("product"); private static readonly InternedString kVersionKey = new InternedString("version"); [Serializable] internal struct MatcherJson { public string @interface; public string[] interfaces; public string deviceClass; public string[] deviceClasses; public string manufacturer; public string[] manufacturers; public string product; public string[] products; public string version; public string[] versions; public Capability[] capabilities; public struct Capability { public string path; public string value; } public static MatcherJson FromMatcher(InputDeviceMatcher matcher) { if (matcher.empty) return new MatcherJson(); var json = new MatcherJson(); foreach (var pattern in matcher.m_Patterns) { var key = pattern.Key; var value = pattern.Value.ToString(); if (key == kInterfaceKey) { if (json.@interface == null) json.@interface = value; else ArrayHelpers.Append(ref json.interfaces, value); } else if (key == kDeviceClassKey) { if (json.deviceClass == null) json.deviceClass = value; else ArrayHelpers.Append(ref json.deviceClasses, value); } else if (key == kManufacturerKey) { if (json.manufacturer == null) json.manufacturer = value; else ArrayHelpers.Append(ref json.manufacturers, value); } else if (key == kProductKey) { if (json.product == null) json.product = value; else ArrayHelpers.Append(ref json.products, value); } else if (key == kVersionKey) { if (json.version == null) json.version = value; else ArrayHelpers.Append(ref json.versions, value); } else { ArrayHelpers.Append(ref json.capabilities, new Capability {path = key, value = value}); } } return json; } public InputDeviceMatcher ToMatcher() { var matcher = new InputDeviceMatcher(); ////TODO: get rid of the piecemeal array allocation and do it in one step // Interfaces. if (!string.IsNullOrEmpty(@interface)) matcher = matcher.WithInterface(@interface); if (interfaces != null) foreach (var value in interfaces) matcher = matcher.WithInterface(value); // Device classes. if (!string.IsNullOrEmpty(deviceClass)) matcher = matcher.WithDeviceClass(deviceClass); if (deviceClasses != null) foreach (var value in deviceClasses) matcher = matcher.WithDeviceClass(value); // Manufacturer. if (!string.IsNullOrEmpty(manufacturer)) matcher = matcher.WithManufacturer(manufacturer); if (manufacturers != null) foreach (var value in manufacturers) matcher = matcher.WithManufacturer(value); // Product. if (!string.IsNullOrEmpty(product)) matcher = matcher.WithProduct(product); if (products != null) foreach (var value in products) matcher = matcher.WithProduct(value); // Version. if (!string.IsNullOrEmpty(version)) matcher = matcher.WithVersion(version); if (versions != null) foreach (var value in versions) matcher = matcher.WithVersion(value); // Capabilities. if (capabilities != null) foreach (var value in capabilities) ////FIXME: we're turning all values into strings here matcher = matcher.WithCapability(value.path, value.value); return matcher; } } } }