123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text.RegularExpressions;
- using UnityEngine.InputSystem.Utilities;
- namespace UnityEngine.InputSystem.Layouts
- {
- /// <summary>
- /// Specification that can be matched against an <see cref="InputDeviceDescription"/>. This is
- /// used to find which <see cref="InputControlLayout"/> to create for a device when it is discovered.
- /// </summary>
- /// <remarks>
- /// 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 <see cref="InputDeviceDescription"/> is <see cref="MatchPercentage"/>.
- ///
- /// Various helper methods such as <see cref="WithInterface"/> or <see cref="WithCapability{TValue}"/>
- /// assist with creating matchers.
- ///
- /// <example>
- /// <code>
- /// // 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.
- /// </code>
- /// </example>
- ///
- /// For each registered <see cref="InputControlLayout"/> 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 <see cref="InputSystem.RegisterLayoutMatcher"/>.
- ///
- /// <example>
- /// <code>
- /// // 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"));
- /// </code>
- /// </example>
- /// </remarks>
- /// <seealso cref="InputDeviceDescription"/>
- /// <seealso cref="InputDevice.description"/>
- /// <seealso cref="InputSystem.RegisterLayoutMatcher"/>
- public struct InputDeviceMatcher : IEquatable<InputDeviceMatcher>
- {
- private KeyValuePair<InternedString, object>[] m_Patterns;
- /// <summary>
- /// If true, the matcher has been default-initialized and contains no
- /// matching <see cref="patterns"/>.
- /// </summary>
- /// <value>Whether the matcher contains any matching patterns.</value>
- /// <seealso cref="patterns"/>
- public bool empty => m_Patterns == null;
- /// <summary>
- /// The list of patterns to match.
- /// </summary>
- /// <value>List of matching patterns.</value>
- /// <remarks>
- /// Each pattern is comprised of a key and a value. The key determines which part
- /// of an <see cref="InputDeviceDescription"/> to match.
- ///
- /// The value represents the expected value. This can be either a plain string
- /// (matched case-insensitive) or a regular expression.
- /// </remarks>
- /// <seealso cref="WithInterface"/>
- /// <seealso cref="WithCapability{TValue}"/>
- /// <seealso cref="WithProduct"/>
- /// <seealso cref="WithManufacturer"/>
- /// <seealso cref="WithVersion"/>
- /// <seealso cref="WithDeviceClass"/>
- public IEnumerable<KeyValuePair<string, object>> patterns
- {
- get
- {
- if (m_Patterns == null)
- yield break;
- var count = m_Patterns.Length;
- for (var i = 0; i < count; ++i)
- yield return new KeyValuePair<string, object>(m_Patterns[i].Key.ToString(), m_Patterns[i].Value);
- }
- }
- /// <summary>
- /// Add a pattern to <see cref="patterns"/> to match an <see cref="InputDeviceDescription.interfaceName"/>.
- /// </summary>
- /// <param name="pattern">String to match.</param>
- /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
- /// a regular expression.</param>
- /// <returns>The modified device matcher with the added pattern.</returns>
- /// <seealso cref="InputDeviceDescription.interfaceName"/>
- public InputDeviceMatcher WithInterface(string pattern, bool supportRegex = true)
- {
- return With(kInterfaceKey, pattern, supportRegex);
- }
- /// <summary>
- /// Add a pattern to <see cref="patterns"/> to match a <see cref="InputDeviceDescription.deviceClass"/>.
- /// </summary>
- /// <param name="pattern">String to match.</param>
- /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
- /// a regular expression.</param>
- /// <returns>The modified device matcher with the added pattern.</returns>
- /// <seealso cref="InputDeviceDescription.deviceClass"/>
- public InputDeviceMatcher WithDeviceClass(string pattern, bool supportRegex = true)
- {
- return With(kDeviceClassKey, pattern, supportRegex);
- }
- /// <summary>
- /// Add a pattern to <see cref="patterns"/> to match a <see cref="InputDeviceDescription.manufacturer"/>.
- /// </summary>
- /// <param name="pattern">String to match.</param>
- /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
- /// a regular expression.</param>
- /// <returns>The modified device matcher with the added pattern.</returns>
- /// <seealso cref="InputDeviceDescription.manufacturer"/>
- public InputDeviceMatcher WithManufacturer(string pattern, bool supportRegex = true)
- {
- return With(kManufacturerKey, pattern, supportRegex);
- }
- /// <summary>
- /// Add a pattern to <see cref="patterns"/> to match a <see cref="InputDeviceDescription.product"/>.
- /// </summary>
- /// <param name="pattern">String to match.</param>
- /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
- /// a regular expression.</param>
- /// <returns>The modified device matcher with the added pattern.</returns>
- /// <seealso cref="InputDeviceDescription.product"/>
- public InputDeviceMatcher WithProduct(string pattern, bool supportRegex = true)
- {
- return With(kProductKey, pattern, supportRegex);
- }
- /// <summary>
- /// Add a pattern to <see cref="patterns"/> to match a <see cref="InputDeviceDescription.version"/>.
- /// </summary>
- /// <param name="pattern">String to match.</param>
- /// <param name="supportRegex">If true (default), <paramref name="pattern"/> can be
- /// a regular expression.</param>
- /// <returns>The modified device matcher with the added pattern.</returns>
- /// <seealso cref="InputDeviceDescription.version"/>
- public InputDeviceMatcher WithVersion(string pattern, bool supportRegex = true)
- {
- return With(kVersionKey, pattern, supportRegex);
- }
- /// <summary>
- /// Add a pattern to <see cref="patterns"/> to match an individual capability in <see cref="InputDeviceDescription.capabilities"/>.
- /// </summary>
- /// <param name="path">Path to the JSON property using '/' as a separator,
- /// e.g. <c>"elements/count"</c>.</param>
- /// <param name="value">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 <c>Mathf.Epsilon</c>. Values are converted between types automatically as
- /// needed (meaning that a bool can be compared to a string, for example).</param>
- /// <typeparam name="TValue">Type of value to match.</typeparam>
- /// <returns>The modified device matcher with the added pattern.</returns>
- /// <remarks>
- /// Capabilities are stored as JSON strings in <see cref="InputDeviceDescription.capabilities"/>.
- /// A matcher has the ability to match specific properties from the JSON object
- /// contained in the capabilities string.
- ///
- /// <example>
- /// <code>
- /// // 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);
- /// </code>
- /// </example>
- ///
- /// Properties in nested objects can be referenced by separating properties
- /// with <c>/</c> and properties in arrays can be indexed with <c>[..]</c>.
- /// </remarks>
- /// <seealso cref="InputDeviceDescription.capabilities"/>
- public InputDeviceMatcher WithCapability<TValue>(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<InternedString, object>(key, value));
- return result;
- }
- /// <summary>
- /// Return the level of matching to the given <paramref name="deviceDescription"/>.
- /// </summary>
- /// <param name="deviceDescription">A device description.</param>
- /// <returns>A score usually in the range between 0 and 1.</returns>
- /// <remarks>
- /// 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 <c>null</c> and not an empty string) in <paramref name="deviceDescription"/> 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.
- ///
- /// <example>
- /// <code>
- /// 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);
- /// </code>
- /// </example>
- /// </remarks>
- 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;
- }
- /// <summary>
- /// Produce a matcher that matches the given device description verbatim.
- /// </summary>
- /// <param name="deviceDescription">A device description.</param>
- /// <returns>A matcher that matches <paramref name="deviceDescription"/> exactly.</returns>
- /// <remarks>
- /// This method can be used to produce a matcher for an existing device description,
- /// e.g. when writing a layout <see cref="InputControlLayout.Builder"/> that produces
- /// layouts for devices on the fly.
- /// </remarks>
- 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;
- }
- /// <summary>
- /// Return a string representation useful for debugging. Lists the
- /// <see cref="patterns"/> contained in the matcher.
- /// </summary>
- /// <returns>A string representation of the matcher.</returns>
- public override string ToString()
- {
- if (empty)
- return "<empty>";
- 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;
- }
- /// <summary>
- /// Test whether this matcher is equivalent to the <paramref name="other"/> matcher.
- /// </summary>
- /// <param name="other">Another device matcher.</param>
- /// <returns>True if the two matchers are equivalent.</returns>
- /// <remarks>
- /// 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.
- /// </remarks>
- [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;
- }
- /// <summary>
- /// Compare this matcher to another.
- /// </summary>
- /// <param name="obj">A matcher object or <c>null</c>.</param>
- /// <returns>True if the matcher is equivalent.</returns>
- /// <seealso cref="Equals(InputDeviceMatcher)"/>
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj))
- return false;
- return obj is InputDeviceMatcher matcher && Equals(matcher);
- }
- /// <summary>
- /// Compare two matchers for equivalence.
- /// </summary>
- /// <param name="left">First device matcher.</param>
- /// <param name="right">Second device matcher.</param>
- /// <returns>True if the two matchers are equivalent.</returns>
- /// <seealso cref="Equals(InputDeviceMatcher)"/>
- public static bool operator==(InputDeviceMatcher left, InputDeviceMatcher right)
- {
- return left.Equals(right);
- }
- /// <summary>
- /// Compare two matchers for non-equivalence.
- /// </summary>
- /// <param name="left">First device matcher.</param>
- /// <param name="right">Second device matcher.</param>
- /// <returns>True if the two matchers are not equivalent.</returns>
- /// <seealso cref="Equals(InputDeviceMatcher)"/>
- public static bool operator!=(InputDeviceMatcher left, InputDeviceMatcher right)
- {
- return !(left == right);
- }
- /// <summary>
- /// Compute a hash code for the device matcher.
- /// </summary>
- /// <returns>A hash code for the matcher.</returns>
- 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;
- }
- }
- }
- }
|