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.OrdinalIgnoreCase) == 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;
}
}
}
}