using System; using UnityEngine.InputSystem.Utilities; ////TODO: add a 'devicePath' property that platforms can use to relay their internal device locators //// (but do *not* take it into account when comparing descriptions for disconnected devices) namespace UnityEngine.InputSystem.Layouts { /// <summary> /// Metadata for an input device. /// </summary> /// <remarks> /// Device descriptions are mainly used to determine which <see cref="InputControlLayout"/> /// to create an actual <see cref="InputDevice"/> instance from. Each description is comprised /// of a set of properties that each are individually optional. However, for a description /// to be usable, at least some need to be set. Generally, the minimum viable description /// for a device is one with <see cref="deviceClass"/> filled out. /// /// <example> /// <code> /// // Device description equivalent to a generic gamepad with no /// // further information about the device. /// new InputDeviceDescription /// { /// deviceClass = "Gamepad" /// }; /// </code> /// </example> /// /// Device descriptions will usually be supplied by the Unity runtime but can also be manually /// fed into the system using <see cref="InputSystem.AddDevice(InputDeviceDescription)"/>. The /// system will remember each device description it has seen regardless of whether it was /// able to successfully create a device from the description. To query the list of descriptions /// that for whatever reason did not result in a device being created, call <see /// cref="InputSystem.GetUnsupportedDevices()"/>. /// /// Whenever layout registrations in the system are changed (e.g. by calling <see /// cref="InputSystem.RegisterLayout{T}"/> or whenever <see cref="InputSettings.supportedDevices"/> /// is changed, the system will go through the list of unsupported devices itself and figure out /// if there are device descriptions that now it can turn into devices. The same also applies /// in reverse; if, for example, a layout is removed that is currently used a device, the /// device will be removed and its description (if any) will be placed on the list of /// unsupported devices. /// </remarks> /// <seealso cref="InputDevice.description"/> /// <seealso cref="InputDeviceMatcher"/> [Serializable] public struct InputDeviceDescription : IEquatable<InputDeviceDescription> { /// <summary> /// How we talk to the device; usually name of the underlying backend that feeds /// state for the device (e.g. "HID" or "XInput"). /// </summary> /// <value>Name of interface through which the device is reported.</value> /// <see cref="InputDeviceMatcher.WithInterface"/> public string interfaceName { get => m_InterfaceName; set => m_InterfaceName = value; } /// <summary> /// What the interface thinks the device classifies as. /// </summary> /// <value>Broad classification of device.</value> /// <remarks> /// If there is no layout specifically matching a device description, /// the device class is used as as fallback. If, for example, this field /// is set to "Gamepad", the "Gamepad" layout is used as a fallback. /// </remarks> /// <seealso cref="InputDeviceMatcher.WithDeviceClass"/> public string deviceClass { get => m_DeviceClass; set => m_DeviceClass = value; } /// <summary> /// Name of the vendor that produced the device. /// </summary> /// <value>Name of manufacturer.</value> /// <seealso cref="InputDeviceMatcher.WithManufacturer"/> public string manufacturer { get => m_Manufacturer; set => m_Manufacturer = value; } /// <summary> /// Name of the product assigned by the vendor to the device. /// </summary> /// <value>Name of product.</value> /// <seealso cref="InputDeviceMatcher.WithProduct"/> public string product { get => m_Product; set => m_Product = value; } /// <summary> /// If available, serial number for the device. /// </summary> /// <value>Serial number of device.</value> public string serial { get => m_Serial; set => m_Serial = value; } /// <summary> /// Version string of the device and/or driver. /// </summary> /// <value>Version of device and/or driver.</value> /// <seealso cref="InputDeviceMatcher.WithVersion"/> public string version { get => m_Version; set => m_Version = value; } /// <summary> /// An optional JSON string listing device-specific capabilities. /// </summary> /// <value>Interface-specific listing of device capabilities.</value> /// <remarks> /// The primary use of this field is to allow custom layout factories /// to create layouts on the fly from in-depth device descriptions delivered /// by external APIs. /// /// In the case of HID, for example, this field contains a JSON representation /// of the HID descriptor (see <see cref="HID.HID.HIDDeviceDescriptor"/>) as /// reported by the device driver. This descriptor contains information about /// all I/O elements on the device which can be used to determine the control /// setup and data format used by the device. /// </remarks> /// <seealso cref="InputDeviceMatcher.WithCapability{T}"/> public string capabilities { get => m_Capabilities; set => m_Capabilities = value; } /// <summary> /// Whether any of the properties in the description are set. /// </summary> /// <value>True if any of <see cref="interfaceName"/>, <see cref="deviceClass"/>, /// <see cref="manufacturer"/>, <see cref="product"/>, <see cref="serial"/>, /// <see cref="version"/>, or <see cref="capabilities"/> is not <c>null</c> and /// not empty.</value> public bool empty => string.IsNullOrEmpty(m_InterfaceName) && string.IsNullOrEmpty(m_DeviceClass) && string.IsNullOrEmpty(m_Manufacturer) && string.IsNullOrEmpty(m_Product) && string.IsNullOrEmpty(m_Serial) && string.IsNullOrEmpty(m_Version) && string.IsNullOrEmpty(m_Capabilities); /// <summary> /// Return a string representation of the description useful for /// debugging. /// </summary> /// <returns>A script representation of the description.</returns> public override string ToString() { var haveProduct = !string.IsNullOrEmpty(product); var haveManufacturer = !string.IsNullOrEmpty(manufacturer); var haveInterface = !string.IsNullOrEmpty(interfaceName); if (haveProduct && haveManufacturer) { if (haveInterface) return $"{manufacturer} {product} ({interfaceName})"; return $"{manufacturer} {product}"; } if (haveProduct) { if (haveInterface) return $"{product} ({interfaceName})"; return product; } if (!string.IsNullOrEmpty(deviceClass)) { if (haveInterface) return $"{deviceClass} ({interfaceName})"; return deviceClass; } // For some HIDs on Windows, we don't get a product and manufacturer string even though // the HID is guaranteed to have a product and vendor ID. Resort to printing capabilities // which for HIDs at least include the product and vendor ID. if (!string.IsNullOrEmpty(capabilities)) { const int kMaxCapabilitiesLength = 40; var caps = capabilities; if (capabilities.Length > kMaxCapabilitiesLength) caps = caps.Substring(0, kMaxCapabilitiesLength) + "..."; if (haveInterface) return $"{caps} ({interfaceName})"; return caps; } if (haveInterface) return interfaceName; return "<Empty Device Description>"; } /// <summary> /// Compare the description to the given <paramref name="other"/> description. /// </summary> /// <param name="other">Another device description.</param> /// <returns>True if the two descriptions are equivalent.</returns> /// <remarks> /// Two descriptions are equivalent if all their properties are equal /// (ignore case). /// </remarks> public bool Equals(InputDeviceDescription other) { return m_InterfaceName.InvariantEqualsIgnoreCase(other.m_InterfaceName) && m_DeviceClass.InvariantEqualsIgnoreCase(other.m_DeviceClass) && m_Manufacturer.InvariantEqualsIgnoreCase(other.m_Manufacturer) && m_Product.InvariantEqualsIgnoreCase(other.m_Product) && m_Serial.InvariantEqualsIgnoreCase(other.m_Serial) && m_Version.InvariantEqualsIgnoreCase(other.m_Version) && ////REVIEW: this would ideally compare JSON contents not just the raw string m_Capabilities.InvariantEqualsIgnoreCase(other.m_Capabilities); } /// <summary> /// Compare the description to the given object. /// </summary> /// <param name="obj">An object.</param> /// <returns>True if <paramref name="obj"/> is an InputDeviceDescription /// equivalent to this one.</returns> /// <seealso cref="Equals(InputDeviceDescription)"/> public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; return obj is InputDeviceDescription description && Equals(description); } /// <summary> /// Compute a hash code for the device description. /// </summary> /// <returns>A hash code.</returns> public override int GetHashCode() { unchecked { var hashCode = m_InterfaceName != null ? m_InterfaceName.GetHashCode() : 0; hashCode = (hashCode * 397) ^ (m_DeviceClass != null ? m_DeviceClass.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (m_Manufacturer != null ? m_Manufacturer.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (m_Product != null ? m_Product.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (m_Serial != null ? m_Serial.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (m_Version != null ? m_Version.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (m_Capabilities != null ? m_Capabilities.GetHashCode() : 0); return hashCode; } } /// <summary> /// Compare the two device descriptions. /// </summary> /// <param name="left">First device description.</param> /// <param name="right">Second device description.</param> /// <returns>True if the two descriptions are equivalent.</returns> /// <seealso cref="Equals(InputDeviceDescription)"/> public static bool operator==(InputDeviceDescription left, InputDeviceDescription right) { return left.Equals(right); } /// <summary> /// Compare the two device descriptions for inequality. /// </summary> /// <param name="left">First device description.</param> /// <param name="right">Second device description.</param> /// <returns>True if the two descriptions are not equivalent.</returns> /// <seealso cref="Equals(InputDeviceDescription)"/> public static bool operator!=(InputDeviceDescription left, InputDeviceDescription right) { return !left.Equals(right); } /// <summary> /// Return a JSON representation of the device description. /// </summary> /// <returns>A JSON representation of the description.</returns> /// <remarks> /// <example> /// The result can be converted back into an InputDeviceDescription /// using <see cref="FromJson"/>. /// /// <code> /// var description = new InputDeviceDescription /// { /// interfaceName = "HID", /// product = "SomeDevice", /// capabilities = @" /// { /// ""vendorId"" : 0xABA, /// ""productId"" : 0xEFE /// } /// " /// }; /// /// Debug.Log(description.ToJson()); /// // Prints /// // { /// // "interface" : "HID", /// // "product" : "SomeDevice", /// // "capabilities" : "{ \"vendorId\" : 0xABA, \"productId\" : 0xEFF }" /// // } /// </code> /// </example> /// </remarks> /// <seealso cref="FromJson"/> public string ToJson() { var data = new DeviceDescriptionJson { @interface = interfaceName, type = deviceClass, product = product, manufacturer = manufacturer, serial = serial, version = version, capabilities = capabilities }; return JsonUtility.ToJson(data, true); } /// <summary> /// Read an InputDeviceDescription from its JSON representation. /// </summary> /// <param name="json">String in JSON format.</param> /// <exception cref="ArgumentNullException"><paramref name="json"/> is <c>null</c>.</exception> /// <returns>The converted </returns> /// <exception cref="ArgumentException">There as a parse error in <paramref name="json"/>. /// </exception> /// <remarks> /// <example> /// <code> /// InputDeviceDescription.FromJson(@" /// { /// ""interface"" : ""HID"", /// ""product"" : ""SomeDevice"" /// } /// "); /// </code> /// </example> /// </remarks> /// <seealso cref="ToJson"/> public static InputDeviceDescription FromJson(string json) { if (json == null) throw new ArgumentNullException(nameof(json)); var data = JsonUtility.FromJson<DeviceDescriptionJson>(json); return new InputDeviceDescription { interfaceName = data.@interface, deviceClass = data.type, product = data.product, manufacturer = data.manufacturer, serial = data.serial, version = data.version, capabilities = data.capabilities }; } internal static bool ComparePropertyToDeviceDescriptor(string propertyName, string propertyValue, string deviceDescriptor) { // We use JsonParser instead of JsonUtility.Parse in order to not allocate GC memory here. var json = new JsonParser(deviceDescriptor); if (!json.NavigateToProperty(propertyName)) { if (string.IsNullOrEmpty(propertyValue)) return true; return false; } return json.CurrentPropertyHasValueEqualTo(propertyValue); } [SerializeField] private string m_InterfaceName; [SerializeField] private string m_DeviceClass; [SerializeField] private string m_Manufacturer; [SerializeField] private string m_Product; [SerializeField] private string m_Serial; [SerializeField] private string m_Version; [SerializeField] private string m_Capabilities; private struct DeviceDescriptionJson { public string @interface; public string type; public string product; public string serial; public string version; public string manufacturer; public string capabilities; } } }