Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Controls /InputControlLayout.cs
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Reflection; | |
| using System.Runtime.CompilerServices; | |
| using System.Runtime.InteropServices; | |
| using System.Runtime.Serialization; | |
| using UnityEngine.InputSystem.LowLevel; | |
| using UnityEngine.InputSystem.Utilities; | |
| ////TODO: *kill* variants! | |
| ////TODO: we really need proper verification to be in place to ensure that the resulting layout isn't coming out with a bad memory layout | |
| ////TODO: add code-generation that takes a layout and spits out C# code that translates it to a common value format | |
| //// (this can be used, for example, to translate all the various gamepad formats into one single common gamepad format) | |
| ////TODO: allow layouts to set default device names | |
| ////TODO: allow creating generic controls as parents just to group child controls | |
| ////TODO: allow things like "-something" and "+something" for usages, processors, etc | |
| ////TODO: allow setting whether the device should automatically become current and whether it wants noise filtering | |
| ////TODO: ensure that if a layout sets a device description, it is indeed a device layout | |
| ////TODO: make offset on InputControlAttribute relative to field instead of relative to entire state struct | |
| ////REVIEW: common usages are on all layouts but only make sense for devices | |
| ////REVIEW: useStateFrom seems like a half-measure; it solves the problem of setting up state blocks but they often also | |
| //// require a specific set of processors | |
| ////REVIEW: Can we allow aliases to be paths rather than just plain names? This would allow changing the hierarchy around while | |
| //// keeping backwards-compatibility. | |
| // Q: Why is there this layout system instead of just new'ing everything up in hand-written C# code? | |
| // A: The current approach has a couple advantages. | |
| // | |
| // * Since it's data-driven, entire layouts can be represented as just data. They can be added to already deployed applications, | |
| // can be sent over the wire, can be analyzed by tools, etc. | |
| // | |
| // * The layouts can be rearranged in powerful ways, even on the fly. Data can be inserted or modified all along the hierarchy | |
| // both from within a layout itself as well as from outside through overrides. The resulting compositions would often be very | |
| // hard/tedious to set up in a linear C# inheritance hierarchy and likely result in repeated reallocation and rearranging of | |
| // already created setups. | |
| // | |
| // * Related to that, the data-driven layouts make it possible to significantly change the data model without requiring changes | |
| // to existing layouts. This, too, would be more complicated if every device would simply new up everything directly. | |
| // | |
| // * We can generate code from them. Means we can, for example, generate code for the DOTS runtime from the same information | |
| // that exists in the input system but without depending on its InputDevice C# implementation. | |
| // | |
| // The biggest drawback, other than code complexity, is that building an InputDevice from an InputControlLayout is slow. | |
| // This is somewhat offset by having a code generator that can "freeze" a specific layout into simple C# code. For these, | |
| // the result is code at least as efficient (but likely *more* efficient) than the equivalent in a code-only layout approach | |
| // while at the same time offering all the advantages of the data-driven approach. | |
| namespace UnityEngine.InputSystem.Layouts | |
| { | |
| /// <summary> | |
| /// Delegate used by <see cref="InputSystem.onFindLayoutForDevice"/>. | |
| /// </summary> | |
| /// <param name="description">The device description supplied by the runtime or through <see | |
| /// cref="InputSystem.AddDevice(InputDeviceDescription)"/>. This is passed by reference instead of | |
| /// by value to allow the callback to fill out fields such as <see cref="InputDeviceDescription.capabilities"/> | |
| /// on the fly based on information queried from external APIs or from the runtime.</param> | |
| /// <param name="matchedLayout">Name of the layout that has been selected for the device or <c>null</c> if | |
| /// no matching layout could be found. Matching is determined from the <see cref="InputDeviceMatcher"/>s for | |
| /// layouts registered in the system.</param> | |
| /// <param name="executeDeviceCommand">A delegate which can be invoked to execute <see cref="InputDeviceCommand"/>s | |
| /// on the device.</param> | |
| /// <returns> Return <c>null</c> or an empty string to indicate that </returns> | |
| /// <remarks> | |
| /// </remarks> | |
| /// <seealso cref="InputSystem.onFindLayoutForDevice"/> | |
| /// <seealso cref="InputSystem.RegisterLayoutBuilder"/> | |
| /// <seealso cref="InputControlLayout"/> | |
| public delegate string InputDeviceFindControlLayoutDelegate(ref InputDeviceDescription description, | |
| string matchedLayout, InputDeviceExecuteCommandDelegate executeDeviceCommand); | |
| /// <summary> | |
| /// A control layout specifies the composition of an <see cref="InputControl"/> or | |
| /// <see cref="InputDevice"/>. | |
| /// </summary> | |
| /// <remarks> | |
| /// Control layouts can be created in three possible ways: | |
| /// | |
| /// <list type="number"> | |
| /// <item><description>Loaded from JSON.</description></item> | |
| /// <item><description>Constructed through reflection from <see cref="InputControl">InputControls</see> classes.</description></item> | |
| /// <item><description>Through layout factories using <see cref="InputControlLayout.Builder"/>.</description></item> | |
| /// </list> | |
| /// | |
| /// Once constructed, control layouts are immutable (but you can always | |
| /// replace a registered layout in the system and it will affect | |
| /// everything constructed from the layout). | |
| /// | |
| /// Control layouts can be for arbitrary control rigs or for entire | |
| /// devices. Device layouts can be matched to <see cref="InputDeviceDescription"> | |
| /// device description</see> using associated <see cref="InputDeviceMatcher"> | |
| /// device matchers</see>. | |
| /// | |
| /// InputControlLayout objects are considered temporaries. Except in the | |
| /// editor, they are not kept around beyond device creation. | |
| /// | |
| /// See the <a href="../manual/Layouts.html">manual</a> for more details on control layouts. | |
| /// </remarks> | |
| public class InputControlLayout | |
| { | |
| private static InternedString s_DefaultVariant = new InternedString("Default"); | |
| public static InternedString DefaultVariant => s_DefaultVariant; | |
| public const string VariantSeparator = ";"; | |
| /// <summary> | |
| /// Specification for the composition of a direct or indirect child control. | |
| /// </summary> | |
| public struct ControlItem | |
| { | |
| /// <summary> | |
| /// Name of the control. Cannot be empty or <c>null</c>. | |
| /// </summary> | |
| /// <value>Name of the control.</value> | |
| /// <remarks> | |
| /// This may also be a path of the form <c>"parentName/childName..."</c>. | |
| /// This can be used to reach inside another layout and modify properties of | |
| /// a control inside of it. An example for this is adding a "leftStick" control | |
| /// using the Stick layout and then adding two control layouts that refer to | |
| /// "leftStick/x" and "leftStick/y" respectively to modify the state format used | |
| /// by the stick. | |
| /// | |
| /// This field is required. | |
| /// </remarks> | |
| /// <seealso cref="isModifyingExistingControl"/> | |
| /// <seealso cref="InputControlAttribute.name"/> | |
| public InternedString name { get; internal set; } | |
| /// <summary> | |
| /// Name of the layout to use for the control. | |
| /// </summary> | |
| /// <value>Name of layout to use.</value> | |
| /// <remarks> | |
| /// Must be the name of a control layout, not device layout. | |
| /// | |
| /// An example would be "Stick". | |
| /// </remarks> | |
| /// <seealso cref="InputSystem.RegisterLayout(Type,string,Nullable{InputDeviceMatcher}"/> | |
| public InternedString layout { get; internal set; } | |
| public InternedString variants { get; internal set; } | |
| public string useStateFrom { get; internal set; } | |
| /// <summary> | |
| /// Optional display name of the control. | |
| /// </summary> | |
| /// <seealso cref="InputControl.displayName"/> | |
| public string displayName { get; internal set; } | |
| /// <summary> | |
| /// Optional abbreviated display name of the control. | |
| /// </summary> | |
| /// <seealso cref="InputControl.shortDisplayName"/> | |
| public string shortDisplayName { get; internal set; } | |
| public ReadOnlyArray<InternedString> usages { get; internal set; } | |
| public ReadOnlyArray<InternedString> aliases { get; internal set; } | |
| public ReadOnlyArray<NamedValue> parameters { get; internal set; } | |
| public ReadOnlyArray<NameAndParameters> processors { get; internal set; } | |
| public uint offset { get; internal set; } | |
| public uint bit { get; internal set; } | |
| public uint sizeInBits { get; internal set; } | |
| public FourCC format { get; internal set; } | |
| private Flags flags { get; set; } | |
| public int arraySize { get; internal set; } | |
| /// <summary> | |
| /// Optional default value for the state memory associated with the control. | |
| /// </summary> | |
| public PrimitiveValue defaultState { get; internal set; } | |
| public PrimitiveValue minValue { get; internal set; } | |
| public PrimitiveValue maxValue { get; internal set; } | |
| /// <summary> | |
| /// If true, the item will not add a control but rather a modify a control | |
| /// inside the hierarchy added by <see cref="layout"/>. This allows, for example, to modify | |
| /// just the X axis control of the left stick directly from within a gamepad | |
| /// layout instead of having to have a custom stick layout for the left stick | |
| /// than in turn would have to make use of a custom axis layout for the X axis. | |
| /// Instead, you can just have a control layout with the name <c>"leftStick/x"</c>. | |
| /// </summary> | |
| public bool isModifyingExistingControl | |
| { | |
| get => (flags & Flags.isModifyingExistingControl) == Flags.isModifyingExistingControl; | |
| internal set | |
| { | |
| if (value) | |
| flags |= Flags.isModifyingExistingControl; | |
| else | |
| flags &= ~Flags.isModifyingExistingControl; | |
| } | |
| } | |
| /// <summary> | |
| /// Get or set whether to mark the control as noisy. | |
| /// </summary> | |
| /// <value>Whether to mark the control as noisy.</value> | |
| /// <remarks> | |
| /// Noisy controls may generate varying input even without "proper" user interaction. For example, | |
| /// a sensor may generate slightly different input values over time even if in fact the very thing | |
| /// (such as the device orientation) that is being measured is not changing. | |
| /// </remarks> | |
| /// <seealso cref="InputControl.noisy"/> | |
| /// <seealso cref="InputControlAttribute.noisy"/> | |
| public bool isNoisy | |
| { | |
| get => (flags & Flags.IsNoisy) == Flags.IsNoisy; | |
| internal set | |
| { | |
| if (value) | |
| flags |= Flags.IsNoisy; | |
| else | |
| flags &= ~Flags.IsNoisy; | |
| } | |
| } | |
| /// <summary> | |
| /// Get or set whether to mark the control as "synthetic". | |
| /// </summary> | |
| /// <value>Whether to mark the control as synthetic.</value> | |
| /// <remarks> | |
| /// Synthetic controls are artificial controls that provide input but do not correspond to actual controls | |
| /// on the hardware. An example is <see cref="Keyboard.anyKey"/> which is an artificial button that triggers | |
| /// if any key on the keyboard is pressed. | |
| /// </remarks> | |
| /// <seealso cref="InputControl.synthetic"/> | |
| /// <seealso cref="InputControlAttribute.synthetic"/> | |
| public bool isSynthetic | |
| { | |
| get => (flags & Flags.IsSynthetic) == Flags.IsSynthetic; | |
| internal set | |
| { | |
| if (value) | |
| flags |= Flags.IsSynthetic; | |
| else | |
| flags &= ~Flags.IsSynthetic; | |
| } | |
| } | |
| /// <summary> | |
| /// Get or set whether the control should be excluded when performing a device reset. | |
| /// </summary> | |
| /// <value>If true, the control will not get reset in a device reset. Off by default.</value> | |
| /// <remarks> | |
| /// Some controls like, for example, mouse positions do not generally make sense to reset when a | |
| /// device is reset. By setting this flag on, the control's state will be excluded in resets. | |
| /// | |
| /// Note that a full reset can still be forced through <see cref="InputSystem.ResetDevice"/> in | |
| /// which case controls that have this flag set will also get reset. | |
| /// </remarks> | |
| /// <seealso cref="InputSystem.ResetDevice"/> | |
| /// <seealso cref="InputControlAttribute.dontReset"/> | |
| public bool dontReset | |
| { | |
| get => (flags & Flags.DontReset) == Flags.DontReset; | |
| internal set | |
| { | |
| if (value) | |
| flags |= Flags.DontReset; | |
| else | |
| flags &= ~Flags.DontReset; | |
| } | |
| } | |
| /// <summary> | |
| /// Whether the control is introduced by the layout. | |
| /// </summary> | |
| /// <value>If true, the control is first introduced by this layout.</value> | |
| /// <remarks> | |
| /// The value of this property is automatically determined by the input system. | |
| /// </remarks> | |
| public bool isFirstDefinedInThisLayout | |
| { | |
| get => (flags & Flags.IsFirstDefinedInThisLayout) != 0; | |
| internal set | |
| { | |
| if (value) | |
| flags |= Flags.IsFirstDefinedInThisLayout; | |
| else | |
| flags &= ~Flags.IsFirstDefinedInThisLayout; | |
| } | |
| } | |
| public bool isArray => (arraySize != 0); | |
| /// <summary> | |
| /// For any property not set on this control layout, take the setting from <paramref name="other"/>. | |
| /// </summary> | |
| /// <param name="other">Control layout providing settings.</param> | |
| /// <remarks> | |
| /// <see cref="name"/> will not be touched. | |
| /// </remarks> | |
| /// <seealso cref="InputControlLayout.MergeLayout"/> | |
| public ControlItem Merge(ControlItem other) | |
| { | |
| var result = new ControlItem(); | |
| result.name = name; | |
| Debug.Assert(!name.IsEmpty(), "Name must not be empty"); | |
| result.isModifyingExistingControl = isModifyingExistingControl; | |
| result.displayName = string.IsNullOrEmpty(displayName) ? other.displayName : displayName; | |
| result.shortDisplayName = string.IsNullOrEmpty(shortDisplayName) ? other.shortDisplayName : shortDisplayName; | |
| result.layout = layout.IsEmpty() ? other.layout : layout; | |
| result.variants = variants.IsEmpty() ? other.variants : variants; | |
| result.useStateFrom = useStateFrom ?? other.useStateFrom; | |
| result.arraySize = !isArray ? other.arraySize : arraySize; | |
| ////FIXME: allow overrides to unset this | |
| result.isNoisy = isNoisy || other.isNoisy; | |
| result.dontReset = dontReset || other.dontReset; | |
| result.isSynthetic = isSynthetic || other.isSynthetic; | |
| result.isFirstDefinedInThisLayout = false; | |
| if (offset != InputStateBlock.InvalidOffset) | |
| result.offset = offset; | |
| else | |
| result.offset = other.offset; | |
| if (bit != InputStateBlock.InvalidOffset) | |
| result.bit = bit; | |
| else | |
| result.bit = other.bit; | |
| if (format != 0) | |
| result.format = format; | |
| else | |
| result.format = other.format; | |
| if (sizeInBits != 0) | |
| result.sizeInBits = sizeInBits; | |
| else | |
| result.sizeInBits = other.sizeInBits; | |
| if (aliases.Count > 0) | |
| result.aliases = aliases; | |
| else | |
| result.aliases = other.aliases; | |
| if (usages.Count > 0) | |
| result.usages = usages; | |
| else | |
| result.usages = other.usages; | |
| ////FIXME: this should properly merge the parameters, not just pick one or the other | |
| //// easiest thing may be to just concatenate the two strings | |
| if (parameters.Count == 0) | |
| result.parameters = other.parameters; | |
| else | |
| result.parameters = parameters; | |
| if (processors.Count == 0) | |
| result.processors = other.processors; | |
| else | |
| result.processors = processors; | |
| if (!string.IsNullOrEmpty(displayName)) | |
| result.displayName = displayName; | |
| else | |
| result.displayName = other.displayName; | |
| if (!defaultState.isEmpty) | |
| result.defaultState = defaultState; | |
| else | |
| result.defaultState = other.defaultState; | |
| if (!minValue.isEmpty) | |
| result.minValue = minValue; | |
| else | |
| result.minValue = other.minValue; | |
| if (!maxValue.isEmpty) | |
| result.maxValue = maxValue; | |
| else | |
| result.maxValue = other.maxValue; | |
| return result; | |
| } | |
| [] | |
| private enum Flags | |
| { | |
| isModifyingExistingControl = 1 << 0, | |
| IsNoisy = 1 << 1, | |
| IsSynthetic = 1 << 2, | |
| IsFirstDefinedInThisLayout = 1 << 3, | |
| DontReset = 1 << 4, | |
| } | |
| } | |
| // Unique name of the layout. | |
| // NOTE: Case-insensitive. | |
| public InternedString name => m_Name; | |
| public string displayName => m_DisplayName ?? m_Name; | |
| public Type type => m_Type; | |
| public InternedString variants => m_Variants; | |
| public FourCC stateFormat => m_StateFormat; | |
| public int stateSizeInBytes => m_StateSizeInBytes; | |
| public IEnumerable<InternedString> baseLayouts => m_BaseLayouts; | |
| public IEnumerable<InternedString> appliedOverrides => m_AppliedOverrides; | |
| public ReadOnlyArray<InternedString> commonUsages => new ReadOnlyArray<InternedString>(m_CommonUsages); | |
| /// <summary> | |
| /// List of child controls defined for the layout. | |
| /// </summary> | |
| /// <value>Child controls defined for the layout.</value> | |
| public ReadOnlyArray<ControlItem> controls => new ReadOnlyArray<ControlItem>(m_Controls); | |
| ////FIXME: this should be a `bool?` | |
| public bool updateBeforeRender => m_UpdateBeforeRender ?? false; | |
| public bool isDeviceLayout => typeof(InputDevice).IsAssignableFrom(m_Type); | |
| public bool isControlLayout => !isDeviceLayout; | |
| /// <summary> | |
| /// Whether the layout is applies overrides to other layouts instead of | |
| /// defining a layout by itself. | |
| /// </summary> | |
| /// <value>True if the layout acts as an override.</value> | |
| /// <seealso cref="InputSystem.RegisterLayoutOverride"/> | |
| public bool isOverride | |
| { | |
| get => (m_Flags & Flags.IsOverride) != 0; | |
| internal set | |
| { | |
| if (value) | |
| m_Flags |= Flags.IsOverride; | |
| else | |
| m_Flags &= ~Flags.IsOverride; | |
| } | |
| } | |
| public bool isGenericTypeOfDevice | |
| { | |
| get => (m_Flags & Flags.IsGenericTypeOfDevice) != 0; | |
| internal set | |
| { | |
| if (value) | |
| m_Flags |= Flags.IsGenericTypeOfDevice; | |
| else | |
| m_Flags &= ~Flags.IsGenericTypeOfDevice; | |
| } | |
| } | |
| public bool hideInUI | |
| { | |
| get => (m_Flags & Flags.HideInUI) != 0; | |
| internal set | |
| { | |
| if (value) | |
| m_Flags |= Flags.HideInUI; | |
| else | |
| m_Flags &= ~Flags.HideInUI; | |
| } | |
| } | |
| /// <summary> | |
| /// Mark the input device created from this layout as noisy, irrespective of whether or not any | |
| /// of its controls have been marked as noisy. | |
| /// </summary> | |
| /// <seealso cref="InputControlLayoutAttribute.isNoisy"/> | |
| public bool isNoisy | |
| { | |
| get => (m_Flags & Flags.IsNoisy) != 0; | |
| internal set | |
| { | |
| if (value) | |
| m_Flags |= Flags.IsNoisy; | |
| else | |
| m_Flags &= ~Flags.IsNoisy; | |
| } | |
| } | |
| /// <summary> | |
| /// Override value for <see cref="InputDevice.canRunInBackground"/>. If this is set by the | |
| /// layout, it will prevent <see cref="QueryCanRunInBackground"/> from being issued. However, other | |
| /// logic that affects <see cref="InputDevice.canRunInBackground"/> may still force a specific value | |
| /// on a device regardless of what's set in the layout. | |
| /// </summary> | |
| /// <seealso cref="InputDevice.canRunInBackground"/> | |
| /// <seealso cref="InputSettings.backgroundBehavior"/> | |
| public bool? canRunInBackground | |
| { | |
| get => (m_Flags & Flags.CanRunInBackgroundIsSet) != 0 ? (bool?)((m_Flags & Flags.CanRunInBackground) != 0) : null; | |
| internal set | |
| { | |
| if (!value.HasValue) | |
| { | |
| m_Flags &= ~Flags.CanRunInBackgroundIsSet; | |
| } | |
| else | |
| { | |
| m_Flags |= Flags.CanRunInBackgroundIsSet; | |
| if (value.Value) | |
| m_Flags |= Flags.CanRunInBackground; | |
| else | |
| m_Flags &= ~Flags.CanRunInBackground; | |
| } | |
| } | |
| } | |
| public ControlItem this[string path] | |
| { | |
| get | |
| { | |
| if (string.IsNullOrEmpty(path)) | |
| throw new ArgumentNullException(nameof(path)); | |
| // Does not use FindControl so that we don't force-intern the given path string. | |
| if (m_Controls != null) | |
| { | |
| for (var i = 0; i < m_Controls.Length; ++i) | |
| { | |
| if (m_Controls[i].name == path) | |
| return m_Controls[i]; | |
| } | |
| } | |
| throw new KeyNotFoundException($"Cannot find control '{path}' in layout '{name}'"); | |
| } | |
| } | |
| public ControlItem? FindControl(InternedString path) | |
| { | |
| if (string.IsNullOrEmpty(path)) | |
| throw new ArgumentNullException(nameof(path)); | |
| if (m_Controls == null) | |
| return null; | |
| for (var i = 0; i < m_Controls.Length; ++i) | |
| { | |
| if (m_Controls[i].name == path) | |
| return m_Controls[i]; | |
| } | |
| return null; | |
| } | |
| public ControlItem? FindControlIncludingArrayElements(string path, out int arrayIndex) | |
| { | |
| if (string.IsNullOrEmpty(path)) | |
| throw new ArgumentNullException(nameof(path)); | |
| arrayIndex = -1; | |
| if (m_Controls == null) | |
| return null; | |
| var arrayIndexAccumulated = 0; | |
| var lastDigitIndex = path.Length; | |
| while (lastDigitIndex > 0 && char.IsDigit(path[lastDigitIndex - 1])) | |
| { | |
| --lastDigitIndex; | |
| arrayIndexAccumulated *= 10; | |
| arrayIndexAccumulated += path[lastDigitIndex] - '0'; | |
| } | |
| var arrayNameLength = 0; | |
| if (lastDigitIndex < path.Length && lastDigitIndex > 0) // Protect against name being all digits. | |
| arrayNameLength = lastDigitIndex; | |
| for (var i = 0; i < m_Controls.Length; ++i) | |
| { | |
| ref var control = ref m_Controls[i]; | |
| if (string.Compare(control.name, path, StringComparison.InvariantCultureIgnoreCase) == 0) | |
| return control; | |
| ////FIXME: what this can't handle is "outerArray4/innerArray5"; not sure we care, though | |
| // NOTE: This will *not* match something like "touch4/tap". Which is what we want. | |
| // In case there is a ControlItem | |
| if (control.isArray && arrayNameLength > 0 && arrayNameLength == control.name.length && | |
| string.Compare(control.name.ToString(), 0, path, 0, arrayNameLength, | |
| StringComparison.InvariantCultureIgnoreCase) == 0) | |
| { | |
| arrayIndex = arrayIndexAccumulated; | |
| return control; | |
| } | |
| } | |
| return null; | |
| } | |
| /// <summary> | |
| /// Return the type of values produced by controls created from the layout. | |
| /// </summary> | |
| /// <returns>The value type of the control or null if it cannot be determined.</returns> | |
| /// <remarks> | |
| /// This method only returns the statically inferred value type. This type corresponds | |
| /// to the type argument to <see cref="InputControl{TValue}"/> in the inheritance hierarchy | |
| /// of <see cref="type"/>. As the type used by the layout may not inherit from | |
| /// <see cref="InputControl{TValue}"/>, this may mean that the value type cannot be inferred | |
| /// and the method will return null. | |
| /// </remarks> | |
| /// <seealso cref="InputControl.valueType"/> | |
| public Type GetValueType() | |
| { | |
| return TypeHelpers.GetGenericTypeArgumentFromHierarchy(type, typeof(InputControl<>), 0); | |
| } | |
| /// <summary> | |
| /// Build a layout programmatically. Primarily for use by layout builders | |
| /// registered with the system. | |
| /// </summary> | |
| /// <seealso cref="InputSystem.RegisterLayoutBuilder"/> | |
| public class Builder | |
| { | |
| /// <summary> | |
| /// Name to assign to the layout. | |
| /// </summary> | |
| /// <value>Name to assign to the layout.</value> | |
| /// <seealso cref="InputControlLayout.name"/> | |
| public string name { get; set; } | |
| /// <summary> | |
| /// Display name to assign to the layout. | |
| /// </summary> | |
| /// <value>Display name to assign to the layout</value> | |
| /// <seealso cref="InputControlLayout.displayName"/> | |
| public string displayName { get; set; } | |
| /// <summary> | |
| /// <see cref="InputControl"/> type to instantiate for the layout. | |
| /// </summary> | |
| /// <value>Control type to instantiate for the layout.</value> | |
| /// <seealso cref="InputControlLayout.type"/> | |
| public Type type { get; set; } | |
| /// <summary> | |
| /// Memory format FourCC code to apply to state memory used by the | |
| /// layout. | |
| /// </summary> | |
| /// <value>FourCC memory format tag.</value> | |
| /// <seealso cref="InputControlLayout.stateFormat"/> | |
| /// <seealso cref="InputStateBlock.format"/> | |
| public FourCC stateFormat { get; set; } | |
| /// <summary> | |
| /// Total size of memory used by the layout. | |
| /// </summary> | |
| /// <value>Size of memory used by the layout.</value> | |
| /// <seealso cref="InputControlLayout.stateSizeInBytes"/> | |
| public int stateSizeInBytes { get; set; } | |
| /// <summary> | |
| /// Which layout to base this layout on. | |
| /// </summary> | |
| /// <value>Name of base layout.</value> | |
| /// <seealso cref="InputControlLayout.baseLayouts"/> | |
| public string extendsLayout | |
| { | |
| get => m_ExtendsLayout; | |
| set | |
| { | |
| if (!string.IsNullOrEmpty(value)) | |
| m_ExtendsLayout = value; | |
| else | |
| m_ExtendsLayout = null; | |
| } | |
| } | |
| private string m_ExtendsLayout; | |
| /// <summary> | |
| /// For device layouts, whether the device wants an extra update | |
| /// before rendering. | |
| /// </summary> | |
| /// <value>True if before-render updates should be enabled for the device.</value> | |
| /// <seealso cref="InputDevice.updateBeforeRender"/> | |
| /// <seealso cref="InputControlLayout.updateBeforeRender"/> | |
| public bool? updateBeforeRender { get; set; } | |
| /// <summary> | |
| /// List of control items set up by the layout. | |
| /// </summary> | |
| /// <value>Controls set up by the layout.</value> | |
| /// <seealso cref="AddControl"/> | |
| public ReadOnlyArray<ControlItem> controls => new ReadOnlyArray<ControlItem>(m_Controls, 0, m_ControlCount); | |
| private int m_ControlCount; | |
| private ControlItem[] m_Controls; | |
| /// <summary> | |
| /// Syntax for configuring an individual <see cref="ControlItem"/>. | |
| /// </summary> | |
| public struct ControlBuilder | |
| { | |
| internal Builder builder; | |
| internal int index; | |
| public ControlBuilder WithDisplayName(string displayName) | |
| { | |
| builder.m_Controls[index].displayName = displayName; | |
| return this; | |
| } | |
| public ControlBuilder WithLayout(string layout) | |
| { | |
| if (string.IsNullOrEmpty(layout)) | |
| throw new ArgumentException("Layout name cannot be null or empty", nameof(layout)); | |
| builder.m_Controls[index].layout = new InternedString(layout); | |
| return this; | |
| } | |
| public ControlBuilder WithFormat(FourCC format) | |
| { | |
| builder.m_Controls[index].format = format; | |
| return this; | |
| } | |
| public ControlBuilder WithFormat(string format) | |
| { | |
| return WithFormat(new FourCC(format)); | |
| } | |
| public ControlBuilder WithByteOffset(uint offset) | |
| { | |
| builder.m_Controls[index].offset = offset; | |
| return this; | |
| } | |
| public ControlBuilder WithBitOffset(uint bit) | |
| { | |
| builder.m_Controls[index].bit = bit; | |
| return this; | |
| } | |
| public ControlBuilder IsSynthetic(bool value) | |
| { | |
| builder.m_Controls[index].isSynthetic = value; | |
| return this; | |
| } | |
| public ControlBuilder IsNoisy(bool value) | |
| { | |
| builder.m_Controls[index].isNoisy = value; | |
| return this; | |
| } | |
| public ControlBuilder DontReset(bool value) | |
| { | |
| builder.m_Controls[index].dontReset = value; | |
| return this; | |
| } | |
| public ControlBuilder WithSizeInBits(uint sizeInBits) | |
| { | |
| builder.m_Controls[index].sizeInBits = sizeInBits; | |
| return this; | |
| } | |
| public ControlBuilder WithRange(float minValue, float maxValue) | |
| { | |
| builder.m_Controls[index].minValue = minValue; | |
| builder.m_Controls[index].maxValue = maxValue; | |
| return this; | |
| } | |
| public ControlBuilder WithUsages(params InternedString[] usages) | |
| { | |
| if (usages == null || usages.Length == 0) | |
| return this; | |
| for (var i = 0; i < usages.Length; ++i) | |
| if (usages[i].IsEmpty()) | |
| throw new ArgumentException( | |
| $"Empty usage entry at index {i} for control '{builder.m_Controls[index].name}' in layout '{builder.name}'", | |
| nameof(usages)); | |
| builder.m_Controls[index].usages = new ReadOnlyArray<InternedString>(usages); | |
| return this; | |
| } | |
| public ControlBuilder WithUsages(IEnumerable<string> usages) | |
| { | |
| var usagesArray = usages.Select(x => new InternedString(x)).ToArray(); | |
| return WithUsages(usagesArray); | |
| } | |
| public ControlBuilder WithUsages(params string[] usages) | |
| { | |
| return WithUsages((IEnumerable<string>)usages); | |
| } | |
| public ControlBuilder WithParameters(string parameters) | |
| { | |
| if (string.IsNullOrEmpty(parameters)) | |
| return this; | |
| var parsed = NamedValue.ParseMultiple(parameters); | |
| builder.m_Controls[index].parameters = new ReadOnlyArray<NamedValue>(parsed); | |
| return this; | |
| } | |
| public ControlBuilder WithProcessors(string processors) | |
| { | |
| if (string.IsNullOrEmpty(processors)) | |
| return this; | |
| var parsed = NameAndParameters.ParseMultiple(processors).ToArray(); | |
| builder.m_Controls[index].processors = new ReadOnlyArray<NameAndParameters>(parsed); | |
| return this; | |
| } | |
| public ControlBuilder WithDefaultState(PrimitiveValue value) | |
| { | |
| builder.m_Controls[index].defaultState = value; | |
| return this; | |
| } | |
| public ControlBuilder UsingStateFrom(string path) | |
| { | |
| if (string.IsNullOrEmpty(path)) | |
| return this; | |
| builder.m_Controls[index].useStateFrom = path; | |
| return this; | |
| } | |
| public ControlBuilder AsArrayOfControlsWithSize(int arraySize) | |
| { | |
| builder.m_Controls[index].arraySize = arraySize; | |
| return this; | |
| } | |
| } | |
| // This invalidates the ControlBuilders from previous calls! (our array may move) | |
| /// <summary> | |
| /// Add a new control to the layout. | |
| /// </summary> | |
| /// <param name="name">Name or path of the control. If it is a path (e.g. <c>"leftStick/x"</c>, | |
| /// then the control either modifies the setup of a child control of another control in the layout | |
| /// or adds a new child control to another control in the layout. Modifying child control is useful, | |
| /// for example, to alter the state format of controls coming from the base layout. Likewise, | |
| /// adding child controls to another control is useful to modify the setup of of the control layout | |
| /// being used without having to create and register a custom control layout.</param> | |
| /// <returns>A control builder that permits setting various parameters on the control.</returns> | |
| /// <exception cref="ArgumentException"><paramref name="name"/> is null or empty.</exception> | |
| public ControlBuilder AddControl(string name) | |
| { | |
| if (string.IsNullOrEmpty(name)) | |
| throw new ArgumentException(name); | |
| var index = ArrayHelpers.AppendWithCapacity(ref m_Controls, ref m_ControlCount, | |
| new ControlItem | |
| { | |
| name = new InternedString(name), | |
| isModifyingExistingControl = name.IndexOf('/') != -1, | |
| offset = InputStateBlock.InvalidOffset, | |
| bit = InputStateBlock.InvalidOffset | |
| }); | |
| return new ControlBuilder | |
| { | |
| builder = this, | |
| index = index | |
| }; | |
| } | |
| public Builder WithName(string name) | |
| { | |
| this.name = name; | |
| return this; | |
| } | |
| public Builder WithDisplayName(string displayName) | |
| { | |
| this.displayName = displayName; | |
| return this; | |
| } | |
| public Builder WithType<T>() | |
| where T : InputControl | |
| { | |
| type = typeof(T); | |
| return this; | |
| } | |
| public Builder WithFormat(FourCC format) | |
| { | |
| stateFormat = format; | |
| return this; | |
| } | |
| public Builder WithFormat(string format) | |
| { | |
| return WithFormat(new FourCC(format)); | |
| } | |
| public Builder WithSizeInBytes(int sizeInBytes) | |
| { | |
| stateSizeInBytes = sizeInBytes; | |
| return this; | |
| } | |
| public Builder Extend(string baseLayoutName) | |
| { | |
| extendsLayout = baseLayoutName; | |
| return this; | |
| } | |
| public InputControlLayout Build() | |
| { | |
| ControlItem[] controls = null; | |
| if (m_ControlCount > 0) | |
| { | |
| controls = new ControlItem[m_ControlCount]; | |
| Array.Copy(m_Controls, controls, m_ControlCount); | |
| } | |
| // Allow layout to be unnamed. The system will automatically set the | |
| // name that the layout has been registered under. | |
| var layout = | |
| new InputControlLayout(new InternedString(name), | |
| type == null && string.IsNullOrEmpty(extendsLayout) ? typeof(InputDevice) : type) | |
| { | |
| m_DisplayName = displayName, | |
| m_StateFormat = stateFormat, | |
| m_StateSizeInBytes = stateSizeInBytes, | |
| m_BaseLayouts = !string.IsNullOrEmpty(extendsLayout) ? new InlinedArray<InternedString>(new InternedString(extendsLayout)) : default, | |
| m_Controls = controls, | |
| m_UpdateBeforeRender = updateBeforeRender | |
| }; | |
| return layout; | |
| } | |
| } | |
| // Uses reflection to construct a layout from the given type. | |
| // Can be used with both control classes and state structs. | |
| public static InputControlLayout FromType(string name, Type type) | |
| { | |
| var controlLayouts = new List<ControlItem>(); | |
| var layoutAttribute = type.GetCustomAttribute<InputControlLayoutAttribute>(true); | |
| // If there's an InputControlLayoutAttribute on the type that has 'stateType' set, | |
| // add control layouts from its state (if present) instead of from the type. | |
| var stateFormat = new FourCC(); | |
| if (layoutAttribute != null && layoutAttribute.stateType != null) | |
| { | |
| AddControlItems(layoutAttribute.stateType, controlLayouts, name); | |
| // Get state type code from state struct. | |
| if (typeof(IInputStateTypeInfo).IsAssignableFrom(layoutAttribute.stateType)) | |
| { | |
| stateFormat = ((IInputStateTypeInfo)Activator.CreateInstance(layoutAttribute.stateType)).format; | |
| } | |
| } | |
| else | |
| { | |
| // Add control layouts from type contents. | |
| AddControlItems(type, controlLayouts, name); | |
| } | |
| if (layoutAttribute != null && !string.IsNullOrEmpty(layoutAttribute.stateFormat)) | |
| stateFormat = new FourCC(layoutAttribute.stateFormat); | |
| // Determine variants (if any). | |
| var variants = new InternedString(); | |
| if (layoutAttribute != null) | |
| variants = new InternedString(layoutAttribute.variants); | |
| ////TODO: make sure all usages are unique (probably want to have a check method that we can run on json layouts as well) | |
| ////TODO: make sure all paths are unique (only relevant for JSON layouts?) | |
| // Create layout object. | |
| var layout = new InputControlLayout(name, type) | |
| { | |
| m_Controls = controlLayouts.ToArray(), | |
| m_StateFormat = stateFormat, | |
| m_Variants = variants, | |
| m_UpdateBeforeRender = layoutAttribute?.updateBeforeRenderInternal, | |
| isGenericTypeOfDevice = layoutAttribute?.isGenericTypeOfDevice ?? false, | |
| hideInUI = layoutAttribute?.hideInUI ?? false, | |
| m_Description = layoutAttribute?.description, | |
| m_DisplayName = layoutAttribute?.displayName, | |
| canRunInBackground = layoutAttribute?.canRunInBackgroundInternal, | |
| isNoisy = layoutAttribute?.isNoisy ?? false | |
| }; | |
| if (layoutAttribute?.commonUsages != null) | |
| layout.m_CommonUsages = | |
| ArrayHelpers.Select(layoutAttribute.commonUsages, x => new InternedString(x)); | |
| return layout; | |
| } | |
| public string ToJson() | |
| { | |
| var layout = LayoutJson.FromLayout(this); | |
| return JsonUtility.ToJson(layout, true); | |
| } | |
| // Constructs a layout from the given JSON source. | |
| public static InputControlLayout FromJson(string json) | |
| { | |
| var layoutJson = JsonUtility.FromJson<LayoutJson>(json); | |
| return layoutJson.ToLayout(); | |
| } | |
| ////REVIEW: shouldn't state be split between input and output? how does output fit into the layout picture in general? | |
| //// should the control layout alone determine the direction things are going in? | |
| private InternedString m_Name; | |
| private Type m_Type; // For extension chains, we can only discover types after loading multiple layouts, so we make this accessible to InputDeviceBuilder. | |
| private InternedString m_Variants; | |
| private FourCC m_StateFormat; | |
| internal int m_StateSizeInBytes; // Note that this is the combined state size for input and output. | |
| internal bool? m_UpdateBeforeRender; | |
| internal InlinedArray<InternedString> m_BaseLayouts; | |
| private InlinedArray<InternedString> m_AppliedOverrides; | |
| private InternedString[] m_CommonUsages; | |
| internal ControlItem[] m_Controls; | |
| internal string m_DisplayName; | |
| private string m_Description; | |
| private Flags m_Flags; | |
| [] | |
| private enum Flags | |
| { | |
| IsGenericTypeOfDevice = 1 << 0, | |
| HideInUI = 1 << 1, | |
| IsOverride = 1 << 2, | |
| CanRunInBackground = 1 << 3, | |
| CanRunInBackgroundIsSet = 1 << 4, | |
| IsNoisy = 1 << 5 | |
| } | |
| private InputControlLayout(string name, Type type) | |
| { | |
| m_Name = new InternedString(name); | |
| m_Type = type; | |
| } | |
| private static void AddControlItems(Type type, List<ControlItem> controlLayouts, string layoutName) | |
| { | |
| AddControlItemsFromFields(type, controlLayouts, layoutName); | |
| AddControlItemsFromProperties(type, controlLayouts, layoutName); | |
| } | |
| // Add ControlLayouts for every public property in the given type that has | |
| // InputControlAttribute applied to it or has an InputControl-derived value type. | |
| private static void AddControlItemsFromFields(Type type, List<ControlItem> controlLayouts, string layoutName) | |
| { | |
| var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); | |
| AddControlItemsFromMembers(fields, controlLayouts, layoutName); | |
| } | |
| // Add ControlLayouts for every public property in the given type that has | |
| // InputControlAttribute applied to it or has an InputControl-derived value type. | |
| private static void AddControlItemsFromProperties(Type type, List<ControlItem> controlLayouts, string layoutName) | |
| { | |
| var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); | |
| AddControlItemsFromMembers(properties, controlLayouts, layoutName); | |
| } | |
| // Add ControlLayouts for every member in the list that has InputControlAttribute applied to it | |
| // or has an InputControl-derived value type. | |
| private static void AddControlItemsFromMembers(MemberInfo[] members, List<ControlItem> controlItems, string layoutName) | |
| { | |
| foreach (var member in members) | |
| { | |
| // Skip anything declared inside InputControl itself. | |
| // Filters out m_Device etc. | |
| if (member.DeclaringType == typeof(InputControl)) | |
| continue; | |
| var valueType = TypeHelpers.GetValueType(member); | |
| // If the value type of the member is a struct type and implements the IInputStateTypeInfo | |
| // interface, dive inside and look. This is useful for composing states of one another. | |
| if (valueType != null && valueType.IsValueType && typeof(IInputStateTypeInfo).IsAssignableFrom(valueType)) | |
| { | |
| var controlCountBefore = controlItems.Count; | |
| AddControlItems(valueType, controlItems, layoutName); | |
| // If the current member is a field that is embedding the state structure, add | |
| // the field offset to all control layouts that were added from the struct. | |
| var memberAsField = member as FieldInfo; | |
| if (memberAsField != null) | |
| { | |
| var fieldOffset = Marshal.OffsetOf(member.DeclaringType, member.Name).ToInt32(); | |
| var controlCountAfter = controlItems.Count; | |
| for (var i = controlCountBefore; i < controlCountAfter; ++i) | |
| { | |
| var controlLayout = controlItems[i]; | |
| if (controlItems[i].offset != InputStateBlock.InvalidOffset) | |
| { | |
| controlLayout.offset += (uint)fieldOffset; | |
| controlItems[i] = controlLayout; | |
| } | |
| } | |
| } | |
| ////TODO: allow attributes on the member to modify control layouts inside the struct | |
| } | |
| // Look for InputControlAttributes. If they aren't there, the member has to be | |
| // of an InputControl-derived value type. | |
| var attributes = member.GetCustomAttributes<InputControlAttribute>(false).ToArray(); | |
| if (attributes.Length == 0) | |
| { | |
| if (valueType == null || !typeof(InputControl).IsAssignableFrom(valueType)) | |
| continue; | |
| // On properties, we require explicit [InputControl] attributes to | |
| // pick them up. Doing it otherwise has proven to lead too easily to | |
| // situations where you inadvertently add new controls to a layout | |
| // just because you added an InputControl-type property to a class. | |
| if (member is PropertyInfo) | |
| continue; | |
| } | |
| AddControlItemsFromMember(member, attributes, controlItems); | |
| } | |
| } | |
| private static void AddControlItemsFromMember(MemberInfo member, | |
| InputControlAttribute[] attributes, List<ControlItem> controlItems) | |
| { | |
| // InputControlAttribute can be applied multiple times to the same member, | |
| // generating a separate control for each occurrence. However, it can also | |
| // generating a separate control for each occurrence. However, it can also | |
| // not be applied at all in which case we still add a control layout (the | |
| // logic that called us already made sure the member is eligible for this kind | |
| // of operation). | |
| if (attributes.Length == 0) | |
| { | |
| var controlItem = CreateControlItemFromMember(member, null); | |
| controlItems.Add(controlItem); | |
| } | |
| else | |
| { | |
| foreach (var attribute in attributes) | |
| { | |
| var controlItem = CreateControlItemFromMember(member, attribute); | |
| controlItems.Add(controlItem); | |
| } | |
| } | |
| } | |
| private static ControlItem CreateControlItemFromMember(MemberInfo member, InputControlAttribute attribute) | |
| { | |
| ////REVIEW: make sure that the value type of the field and the value type of the control match? | |
| // Determine name. | |
| var name = attribute?.name; | |
| if (string.IsNullOrEmpty(name)) | |
| name = member.Name; | |
| var isModifyingChildControlByPath = name.IndexOf('/') != -1; | |
| // Determine display name. | |
| var displayName = attribute?.displayName; | |
| var shortDisplayName = attribute?.shortDisplayName; | |
| // Determine layout. | |
| var layout = attribute?.layout; | |
| if (string.IsNullOrEmpty(layout) && !isModifyingChildControlByPath && | |
| (!(member is FieldInfo) || member.GetCustomAttribute<FixedBufferAttribute>(false) == null)) // Ignore fixed buffer fields. | |
| { | |
| var valueType = TypeHelpers.GetValueType(member); | |
| layout = InferLayoutFromValueType(valueType); | |
| } | |
| // Determine variants. | |
| string variants = null; | |
| if (attribute != null && !string.IsNullOrEmpty(attribute.variants)) | |
| variants = attribute.variants; | |
| // Determine offset. | |
| var offset = InputStateBlock.InvalidOffset; | |
| if (attribute != null && attribute.offset != InputStateBlock.InvalidOffset) | |
| offset = attribute.offset; | |
| else if (member is FieldInfo && !isModifyingChildControlByPath) | |
| offset = (uint)Marshal.OffsetOf(member.DeclaringType, member.Name).ToInt32(); | |
| // Determine bit offset. | |
| var bit = InputStateBlock.InvalidOffset; | |
| if (attribute != null) | |
| bit = attribute.bit; | |
| ////TODO: if size is not set, determine from type of field | |
| // Determine size. | |
| var sizeInBits = 0u; | |
| if (attribute != null) | |
| sizeInBits = attribute.sizeInBits; | |
| // Determine format. | |
| var format = new FourCC(); | |
| if (attribute != null && !string.IsNullOrEmpty(attribute.format)) | |
| format = new FourCC(attribute.format); | |
| else if (!isModifyingChildControlByPath && bit == InputStateBlock.InvalidOffset) | |
| { | |
| ////REVIEW: this logic makes it hard to inherit settings from the base layout; if we do this stuff, | |
| //// we should probably do it in InputDeviceBuilder and not directly on the layout | |
| var valueType = TypeHelpers.GetValueType(member); | |
| format = InputStateBlock.GetPrimitiveFormatFromType(valueType); | |
| } | |
| // Determine aliases. | |
| InternedString[] aliases = null; | |
| if (attribute != null) | |
| { | |
| var joined = ArrayHelpers.Join(attribute.alias, attribute.aliases); | |
| if (joined != null) | |
| aliases = joined.Select(x => new InternedString(x)).ToArray(); | |
| } | |
| // Determine usages. | |
| InternedString[] usages = null; | |
| if (attribute != null) | |
| { | |
| var joined = ArrayHelpers.Join(attribute.usage, attribute.usages); | |
| if (joined != null) | |
| usages = joined.Select(x => new InternedString(x)).ToArray(); | |
| } | |
| // Determine parameters. | |
| NamedValue[] parameters = null; | |
| if (attribute != null && !string.IsNullOrEmpty(attribute.parameters)) | |
| parameters = NamedValue.ParseMultiple(attribute.parameters); | |
| // Determine processors. | |
| NameAndParameters[] processors = null; | |
| if (attribute != null && !string.IsNullOrEmpty(attribute.processors)) | |
| processors = NameAndParameters.ParseMultiple(attribute.processors).ToArray(); | |
| // Determine whether to use state from another control. | |
| string useStateFrom = null; | |
| if (attribute != null && !string.IsNullOrEmpty(attribute.useStateFrom)) | |
| useStateFrom = attribute.useStateFrom; | |
| // Determine if it's a noisy control. | |
| var isNoisy = false; | |
| if (attribute != null) | |
| isNoisy = attribute.noisy; | |
| // Determine whether it's a dontReset control. | |
| var dontReset = false; | |
| if (attribute != null) | |
| dontReset = attribute.dontReset; | |
| // Determine if it's a synthetic control. | |
| var isSynthetic = false; | |
| if (attribute != null) | |
| isSynthetic = attribute.synthetic; | |
| // Determine array size. | |
| var arraySize = 0; | |
| if (attribute != null) | |
| arraySize = attribute.arraySize; | |
| // Determine default state. | |
| var defaultState = new PrimitiveValue(); | |
| if (attribute != null) | |
| defaultState = PrimitiveValue.FromObject(attribute.defaultState); | |
| // Determine min and max value. | |
| var minValue = new PrimitiveValue(); | |
| var maxValue = new PrimitiveValue(); | |
| if (attribute != null) | |
| { | |
| minValue = PrimitiveValue.FromObject(attribute.minValue); | |
| maxValue = PrimitiveValue.FromObject(attribute.maxValue); | |
| } | |
| return new ControlItem | |
| { | |
| name = new InternedString(name), | |
| displayName = displayName, | |
| shortDisplayName = shortDisplayName, | |
| layout = new InternedString(layout), | |
| variants = new InternedString(variants), | |
| useStateFrom = useStateFrom, | |
| format = format, | |
| offset = offset, | |
| bit = bit, | |
| sizeInBits = sizeInBits, | |
| parameters = new ReadOnlyArray<NamedValue>(parameters), | |
| processors = new ReadOnlyArray<NameAndParameters>(processors), | |
| usages = new ReadOnlyArray<InternedString>(usages), | |
| aliases = new ReadOnlyArray<InternedString>(aliases), | |
| isModifyingExistingControl = isModifyingChildControlByPath, | |
| isFirstDefinedInThisLayout = true, | |
| isNoisy = isNoisy, | |
| dontReset = dontReset, | |
| isSynthetic = isSynthetic, | |
| arraySize = arraySize, | |
| defaultState = defaultState, | |
| minValue = minValue, | |
| maxValue = maxValue, | |
| }; | |
| } | |
| ////REVIEW: this tends to cause surprises; is it worth its cost? | |
| private static string InferLayoutFromValueType(Type type) | |
| { | |
| var layout = s_Layouts.TryFindLayoutForType(type); | |
| if (layout.IsEmpty()) | |
| { | |
| var typeName = new InternedString(type.Name); | |
| if (s_Layouts.HasLayout(typeName)) | |
| layout = typeName; | |
| else if (type.Name.EndsWith("Control")) | |
| { | |
| typeName = new InternedString(type.Name.Substring(0, type.Name.Length - "Control".Length)); | |
| if (s_Layouts.HasLayout(typeName)) | |
| layout = typeName; | |
| } | |
| } | |
| return layout; | |
| } | |
| /// <summary> | |
| /// Merge the settings from <paramref name="other"/> into the layout such that they become | |
| /// the base settings. | |
| /// </summary> | |
| /// <param name="other"></param> | |
| /// <remarks> | |
| /// This is the central method for allowing layouts to 'inherit' settings from their | |
| /// base layout. It will merge the information in <paramref name="other"/> into the current | |
| /// layout such that the existing settings in the current layout acts as if applied on top | |
| /// of the settings in the base layout. | |
| /// </remarks> | |
| public void MergeLayout(InputControlLayout other) | |
| { | |
| if (other == null) | |
| throw new ArgumentNullException(nameof(other)); | |
| m_UpdateBeforeRender = m_UpdateBeforeRender ?? other.m_UpdateBeforeRender; | |
| if (m_Variants.IsEmpty()) | |
| m_Variants = other.m_Variants; | |
| // Determine type. Basically, if the other layout's type is more specific | |
| // than our own, we switch to that one. Otherwise we stay on our own type. | |
| if (m_Type == null) | |
| m_Type = other.m_Type; | |
| else if (m_Type.IsAssignableFrom(other.m_Type)) | |
| m_Type = other.m_Type; | |
| // If the layout has variants set on it, we want to merge away information coming | |
| // from 'other' than isn't relevant to those variants. | |
| var layoutIsTargetingSpecificVariants = !m_Variants.IsEmpty(); | |
| if (m_StateFormat == new FourCC()) | |
| m_StateFormat = other.m_StateFormat; | |
| // Combine common usages. | |
| m_CommonUsages = ArrayHelpers.Merge(other.m_CommonUsages, m_CommonUsages); | |
| // Retain list of overrides. | |
| m_AppliedOverrides.Merge(other.m_AppliedOverrides); | |
| // Inherit display name. | |
| if (string.IsNullOrEmpty(m_DisplayName)) | |
| m_DisplayName = other.m_DisplayName; | |
| // Merge controls. | |
| if (m_Controls == null) | |
| { | |
| m_Controls = other.m_Controls; | |
| } | |
| else if (other.m_Controls != null) | |
| { | |
| var baseControls = other.m_Controls; | |
| // Even if the counts match we don't know how many controls are in the | |
| // set until we actually gone through both control lists and looked at | |
| // the names. | |
| var controls = new List<ControlItem>(); | |
| var baseControlVariants = new List<string>(); | |
| ////REVIEW: should setting variants directly on a layout force that variant to automatically | |
| //// be set on every control item directly defined in that layout? | |
| var baseControlTable = CreateLookupTableForControls(baseControls, baseControlVariants); | |
| var thisControlTable = CreateLookupTableForControls(m_Controls); | |
| // First go through every control we have in this layout. Add every control from | |
| // `thisControlTable` while removing corresponding control items from `baseControlTable`. | |
| foreach (var pair in thisControlTable) | |
| { | |
| if (baseControlTable.TryGetValue(pair.Key, out var baseControlItem)) | |
| { | |
| var mergedLayout = pair.Value.Merge(baseControlItem); | |
| controls.Add(mergedLayout); | |
| // Remove the entry so we don't hit it again in the pass through | |
| // baseControlTable below. | |
| baseControlTable.Remove(pair.Key); | |
| } | |
| ////REVIEW: is this really the most useful behavior? | |
| // We may be looking at a control that is using variants on the base layout but | |
| // isn't targeting specific variants on the derived layout. In that case, we | |
| // want to take each of the variants from the base layout and merge them with | |
| // the control layout in the derived layout. | |
| else if (pair.Value.variants.IsEmpty() || pair.Value.variants == DefaultVariant) | |
| { | |
| var isTargetingVariants = false; | |
| if (layoutIsTargetingSpecificVariants) | |
| { | |
| // We're only looking for specific variants so try only that those. | |
| for (var i = 0; i < baseControlVariants.Count; ++i) | |
| { | |
| if (VariantsMatch(m_Variants.ToLower(), baseControlVariants[i])) | |
| { | |
| var key = $"{pair.Key}@{baseControlVariants[i]}"; | |
| if (baseControlTable.TryGetValue(key, out baseControlItem)) | |
| { | |
| var mergedLayout = pair.Value.Merge(baseControlItem); | |
| controls.Add(mergedLayout); | |
| baseControlTable.Remove(key); | |
| isTargetingVariants = true; | |
| } | |
| } | |
| } | |
| } | |
| else | |
| { | |
| // Try each variants present in the base layout. | |
| foreach (var variant in baseControlVariants) | |
| { | |
| var key = $"{pair.Key}@{variant}"; | |
| if (baseControlTable.TryGetValue(key, out baseControlItem)) | |
| { | |
| var mergedLayout = pair.Value.Merge(baseControlItem); | |
| controls.Add(mergedLayout); | |
| baseControlTable.Remove(key); | |
| isTargetingVariants = true; | |
| } | |
| } | |
| } | |
| // Okay, this control item isn't corresponding to anything in the base layout | |
| // so just add it as is. | |
| if (!isTargetingVariants) | |
| controls.Add(pair.Value); | |
| } | |
| // We may be looking at a control that is targeting a specific variant | |
| // in this layout but not targeting a variant in the base layout. We still want to | |
| // merge information from that non-targeted base control. | |
| else if (baseControlTable.TryGetValue(pair.Value.name.ToLower(), out baseControlItem)) | |
| { | |
| var mergedLayout = pair.Value.Merge(baseControlItem); | |
| controls.Add(mergedLayout); | |
| baseControlTable.Remove(pair.Value.name.ToLower()); | |
| } | |
| // Seems like we can't match it to a control in the base layout. We already know it | |
| // must have a variants setting (because we checked above) so if the variants setting | |
| // doesn't prevent us, just include the control. It's most likely a path-modifying | |
| // control (e.g. "rightStick/x"). | |
| else if (VariantsMatch(m_Variants, pair.Value.variants)) | |
| { | |
| controls.Add(pair.Value); | |
| } | |
| } | |
| // And then go through all the controls in the base and take the | |
| // ones we're missing. We've already removed all the ones that intersect | |
| // and had to be merged so the rest we can just slurp into the list as is. | |
| if (!layoutIsTargetingSpecificVariants) | |
| { | |
| var indexStart = controls.Count; | |
| controls.AddRange(baseControlTable.Values); | |
| // Mark the controls as being inherited. | |
| for (var i = indexStart; i < controls.Count; ++i) | |
| { | |
| var control = controls[i]; | |
| control.isFirstDefinedInThisLayout = false; | |
| controls[i] = control; | |
| } | |
| } | |
| else | |
| { | |
| // Filter out controls coming from the base layout which are targeting variants | |
| // that we're not interested in. | |
| var indexStart = controls.Count; | |
| controls.AddRange( | |
| baseControlTable.Values.Where(x => VariantsMatch(m_Variants, x.variants))); | |
| // Mark the controls as being inherited. | |
| for (var i = indexStart; i < controls.Count; ++i) | |
| { | |
| var control = controls[i]; | |
| control.isFirstDefinedInThisLayout = false; | |
| controls[i] = control; | |
| } | |
| } | |
| m_Controls = controls.ToArray(); | |
| } | |
| } | |
| private static Dictionary<string, ControlItem> CreateLookupTableForControls( | |
| ControlItem[] controlItems, List<string> variants = null) | |
| { | |
| var table = new Dictionary<string, ControlItem>(); | |
| for (var i = 0; i < controlItems.Length; ++i) | |
| { | |
| var key = controlItems[i].name.ToLower(); | |
| // Need to take variants into account as well. Otherwise two variants for | |
| // "leftStick", for example, will overwrite each other. | |
| var itemVariants = controlItems[i].variants; | |
| if (!itemVariants.IsEmpty() && itemVariants != DefaultVariant) | |
| { | |
| // If there's multiple variants on the control, we add it to the table multiple times. | |
| if (itemVariants.ToString().IndexOf(VariantSeparator[0]) != -1) | |
| { | |
| var itemVariantArray = itemVariants.ToLower().Split(VariantSeparator[0]); | |
| foreach (var name in itemVariantArray) | |
| { | |
| variants?.Add(name); | |
| key = $"{key}@{name}"; | |
| table[key] = controlItems[i]; | |
| } | |
| continue; | |
| } | |
| key = $"{key}@{itemVariants.ToLower()}"; | |
| variants?.Add(itemVariants.ToLower()); | |
| } | |
| table[key] = controlItems[i]; | |
| } | |
| return table; | |
| } | |
| internal static bool VariantsMatch(InternedString expected, InternedString actual) | |
| { | |
| return VariantsMatch(expected.ToLower(), actual.ToLower()); | |
| } | |
| internal static bool VariantsMatch(string expected, string actual) | |
| { | |
| ////REVIEW: does this make sense? | |
| // Default variant works with any other expected variant. | |
| if (actual != null && | |
| StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(DefaultVariant, actual, VariantSeparator[0])) | |
| return true; | |
| // If we don't expect a specific variant, we accept any variant. | |
| if (expected == null) | |
| return true; | |
| // If we there's no variant set on what we actual got, then it matches even if we | |
| // expect specific variants. | |
| if (actual == null) | |
| return true; | |
| // Match if the two variant sets intersect on at least one element. | |
| return StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(expected, actual, VariantSeparator[0]); | |
| } | |
| internal static void ParseHeaderFieldsFromJson(string json, out InternedString name, | |
| out InlinedArray<InternedString> baseLayouts, out InputDeviceMatcher deviceMatcher) | |
| { | |
| var header = JsonUtility.FromJson<LayoutJsonNameAndDescriptorOnly>(json); | |
| name = new InternedString(header.name); | |
| baseLayouts = new InlinedArray<InternedString>(); | |
| if (!string.IsNullOrEmpty(header.extend)) | |
| baseLayouts.Append(new InternedString(header.extend)); | |
| if (header.extendMultiple != null) | |
| foreach (var item in header.extendMultiple) | |
| baseLayouts.Append(new InternedString(item)); | |
| deviceMatcher = header.device.ToMatcher(); | |
| } | |
| [] | |
| internal struct LayoutJsonNameAndDescriptorOnly | |
| { | |
| public string name; | |
| public string extend; | |
| public string[] extendMultiple; | |
| public InputDeviceMatcher.MatcherJson device; | |
| } | |
| [] | |
| private struct LayoutJson | |
| { | |
| // Disable warnings that these fields are never assigned to. They are set | |
| // by JsonUtility. | |
| // ReSharper disable MemberCanBePrivate.Local | |
| public string name; | |
| public string extend; | |
| public string[] extendMultiple; | |
| public string format; | |
| public string beforeRender; // Can't be simple bool as otherwise we can't tell whether it was set or not. | |
| public string runInBackground; | |
| public string[] commonUsages; | |
| public string displayName; | |
| public string description; | |
| public string type; // This is mostly for when we turn arbitrary InputControlLayouts into JSON; less for layouts *coming* from JSON. | |
| public string variant; | |
| public bool isGenericTypeOfDevice; | |
| public bool hideInUI; | |
| public ControlItemJson[] controls; | |
| // ReSharper restore MemberCanBePrivate.Local | |
| public InputControlLayout ToLayout() | |
| { | |
| // By default, the type of the layout is determined from the first layout | |
| // in its 'extend' property chain that has a type set. However, if the layout | |
| // extends nothing, we can't know what type to use for it so we default to | |
| // InputDevice. | |
| Type type = null; | |
| if (!string.IsNullOrEmpty(this.type)) | |
| { | |
| type = Type.GetType(this.type, false); | |
| if (type == null) | |
| { | |
| Debug.Log( | |
| $"Cannot find type '{this.type}' used by layout '{name}'; falling back to using InputDevice"); | |
| type = typeof(InputDevice); | |
| } | |
| else if (!typeof(InputControl).IsAssignableFrom(type)) | |
| { | |
| throw new InvalidOperationException($"'{this.type}' used by layout '{name}' is not an InputControl"); | |
| } | |
| } | |
| else if (string.IsNullOrEmpty(extend)) | |
| type = typeof(InputDevice); | |
| // Create layout. | |
| var layout = new InputControlLayout(name, type) | |
| { | |
| m_DisplayName = displayName, | |
| m_Description = description, | |
| isGenericTypeOfDevice = isGenericTypeOfDevice, | |
| hideInUI = hideInUI, | |
| m_Variants = new InternedString(variant), | |
| m_CommonUsages = ArrayHelpers.Select(commonUsages, x => new InternedString(x)), | |
| }; | |
| if (!string.IsNullOrEmpty(format)) | |
| layout.m_StateFormat = new FourCC(format); | |
| // Base layout. | |
| if (!string.IsNullOrEmpty(extend)) | |
| layout.m_BaseLayouts.Append(new InternedString(extend)); | |
| if (extendMultiple != null) | |
| foreach (var element in extendMultiple) | |
| layout.m_BaseLayouts.Append(new InternedString(element)); | |
| // Before render behavior. | |
| if (!string.IsNullOrEmpty(beforeRender)) | |
| { | |
| var beforeRenderLowerCase = beforeRender.ToLower(); | |
| if (beforeRenderLowerCase == "ignore") | |
| layout.m_UpdateBeforeRender = false; | |
| else if (beforeRenderLowerCase == "update") | |
| layout.m_UpdateBeforeRender = true; | |
| else | |
| throw new InvalidOperationException($"Invalid beforeRender setting '{beforeRender}' (should be 'ignore' or 'update')"); | |
| } | |
| // CanRunInBackground flag. | |
| if (!string.IsNullOrEmpty(runInBackground)) | |
| { | |
| var runInBackgroundLowerCase = runInBackground.ToLower(); | |
| if (runInBackgroundLowerCase == "enabled") | |
| layout.canRunInBackground = true; | |
| else if (runInBackgroundLowerCase == "disabled") | |
| layout.canRunInBackground = false; | |
| else | |
| throw new InvalidOperationException($"Invalid runInBackground setting '{beforeRender}' (should be 'enabled' or 'disabled')"); | |
| } | |
| // Add controls. | |
| if (controls != null) | |
| { | |
| var controlLayouts = new List<ControlItem>(); | |
| foreach (var control in controls) | |
| { | |
| if (string.IsNullOrEmpty(control.name)) | |
| throw new InvalidOperationException($"Control with no name in layout '{name}"); | |
| var controlLayout = control.ToLayout(); | |
| controlLayouts.Add(controlLayout); | |
| } | |
| layout.m_Controls = controlLayouts.ToArray(); | |
| } | |
| return layout; | |
| } | |
| public static LayoutJson FromLayout(InputControlLayout layout) | |
| { | |
| return new LayoutJson | |
| { | |
| name = layout.m_Name, | |
| type = layout.type?.AssemblyQualifiedName, | |
| variant = layout.m_Variants, | |
| displayName = layout.m_DisplayName, | |
| description = layout.m_Description, | |
| isGenericTypeOfDevice = layout.isGenericTypeOfDevice, | |
| hideInUI = layout.hideInUI, | |
| extend = layout.m_BaseLayouts.length == 1 ? layout.m_BaseLayouts[0].ToString() : null, | |
| extendMultiple = layout.m_BaseLayouts.length > 1 ? layout.m_BaseLayouts.ToArray(x => x.ToString()) : null, | |
| format = layout.stateFormat.ToString(), | |
| commonUsages = ArrayHelpers.Select(layout.m_CommonUsages, x => x.ToString()), | |
| controls = ControlItemJson.FromControlItems(layout.m_Controls), | |
| beforeRender = layout.m_UpdateBeforeRender != null ? (layout.m_UpdateBeforeRender.Value ? "Update" : "Ignore") : null, | |
| }; | |
| } | |
| } | |
| // This is a class instead of a struct so that we can assign 'offset' a custom | |
| // default value. Otherwise we can't tell whether the user has actually set it | |
| // or not (0 is a valid offset). Sucks, though, as we now get lots of allocations | |
| // from the control array. | |
| [] | |
| private class ControlItemJson | |
| { | |
| // Disable warnings that these fields are never assigned to. They are set | |
| // by JsonUtility. | |
| // ReSharper disable MemberCanBePrivate.Local | |
| public string name; | |
| public string layout; | |
| public string variants; | |
| public string usage; // Convenience to not have to create array for single usage. | |
| public string alias; // Same. | |
| public string useStateFrom; | |
| public uint offset; | |
| public uint bit; | |
| public uint sizeInBits; | |
| public string format; | |
| public int arraySize; | |
| public string[] usages; | |
| public string[] aliases; | |
| public string parameters; | |
| public string processors; | |
| public string displayName; | |
| public string shortDisplayName; | |
| public bool noisy; | |
| public bool dontReset; | |
| public bool synthetic; | |
| // This should be an object type field and allow any JSON primitive value type as well | |
| // as arrays of those. Unfortunately, the Unity JSON serializer, given it uses Unity serialization | |
| // and thus doesn't support polymorphism, can do no such thing. Hopefully we do get support | |
| // for this later but for now, we use a string-based value fallback instead. | |
| public string defaultState; | |
| public string minValue; | |
| public string maxValue; | |
| // ReSharper restore MemberCanBePrivate.Local | |
| public ControlItemJson() | |
| { | |
| offset = InputStateBlock.InvalidOffset; | |
| bit = InputStateBlock.InvalidOffset; | |
| } | |
| public ControlItem ToLayout() | |
| { | |
| var layout = new ControlItem | |
| { | |
| name = new InternedString(name), | |
| layout = new InternedString(this.layout), | |
| variants = new InternedString(variants), | |
| displayName = displayName, | |
| shortDisplayName = shortDisplayName, | |
| offset = offset, | |
| useStateFrom = useStateFrom, | |
| bit = bit, | |
| sizeInBits = sizeInBits, | |
| isModifyingExistingControl = name.IndexOf('/') != -1, | |
| isNoisy = noisy, | |
| dontReset = dontReset, | |
| isSynthetic = synthetic, | |
| isFirstDefinedInThisLayout = true, | |
| arraySize = arraySize, | |
| }; | |
| if (!string.IsNullOrEmpty(format)) | |
| layout.format = new FourCC(format); | |
| if (!string.IsNullOrEmpty(usage) || usages != null) | |
| { | |
| var usagesList = new List<string>(); | |
| if (!string.IsNullOrEmpty(usage)) | |
| usagesList.Add(usage); | |
| if (usages != null) | |
| usagesList.AddRange(usages); | |
| layout.usages = new ReadOnlyArray<InternedString>(usagesList.Select(x => new InternedString(x)).ToArray()); | |
| } | |
| if (!string.IsNullOrEmpty(alias) || aliases != null) | |
| { | |
| var aliasesList = new List<string>(); | |
| if (!string.IsNullOrEmpty(alias)) | |
| aliasesList.Add(alias); | |
| if (aliases != null) | |
| aliasesList.AddRange(aliases); | |
| layout.aliases = new ReadOnlyArray<InternedString>(aliasesList.Select(x => new InternedString(x)).ToArray()); | |
| } | |
| if (!string.IsNullOrEmpty(parameters)) | |
| layout.parameters = new ReadOnlyArray<NamedValue>(NamedValue.ParseMultiple(parameters)); | |
| if (!string.IsNullOrEmpty(processors)) | |
| layout.processors = new ReadOnlyArray<NameAndParameters>(NameAndParameters.ParseMultiple(processors).ToArray()); | |
| if (defaultState != null) | |
| layout.defaultState = PrimitiveValue.FromObject(defaultState); | |
| if (minValue != null) | |
| layout.minValue = PrimitiveValue.FromObject(minValue); | |
| if (maxValue != null) | |
| layout.maxValue = PrimitiveValue.FromObject(maxValue); | |
| return layout; | |
| } | |
| public static ControlItemJson[] FromControlItems(ControlItem[] items) | |
| { | |
| if (items == null) | |
| return null; | |
| var count = items.Length; | |
| var result = new ControlItemJson[count]; | |
| for (var i = 0; i < count; ++i) | |
| { | |
| var item = items[i]; | |
| result[i] = new ControlItemJson | |
| { | |
| name = item.name, | |
| layout = item.layout, | |
| variants = item.variants, | |
| displayName = item.displayName, | |
| shortDisplayName = item.shortDisplayName, | |
| bit = item.bit, | |
| offset = item.offset, | |
| sizeInBits = item.sizeInBits, | |
| format = item.format.ToString(), | |
| parameters = string.Join(",", item.parameters.Select(x => x.ToString()).ToArray()), | |
| processors = string.Join(",", item.processors.Select(x => x.ToString()).ToArray()), | |
| usages = item.usages.Select(x => x.ToString()).ToArray(), | |
| aliases = item.aliases.Select(x => x.ToString()).ToArray(), | |
| noisy = item.isNoisy, | |
| dontReset = item.dontReset, | |
| synthetic = item.isSynthetic, | |
| arraySize = item.arraySize, | |
| defaultState = item.defaultState.ToString(), | |
| minValue = item.minValue.ToString(), | |
| maxValue = item.maxValue.ToString(), | |
| }; | |
| } | |
| return result; | |
| } | |
| } | |
| internal struct Collection | |
| { | |
| public const float kBaseScoreForNonGeneratedLayouts = 1.0f; | |
| public struct LayoutMatcher | |
| { | |
| public InternedString layoutName; | |
| public InputDeviceMatcher deviceMatcher; | |
| } | |
| public struct PrecompiledLayout | |
| { | |
| public Func<InputDevice> factoryMethod; | |
| public string metadata; | |
| } | |
| public Dictionary<InternedString, Type> layoutTypes; | |
| public Dictionary<InternedString, string> layoutStrings; | |
| public Dictionary<InternedString, Func<InputControlLayout>> layoutBuilders; | |
| public Dictionary<InternedString, InternedString> baseLayoutTable; | |
| public Dictionary<InternedString, InternedString[]> layoutOverrides; | |
| public HashSet<InternedString> layoutOverrideNames; | |
| public Dictionary<InternedString, PrecompiledLayout> precompiledLayouts; | |
| ////TODO: find a smarter approach that doesn't require linearly scanning through all matchers | |
| //// (also ideally shouldn't be a List but with Collection being a struct and given how it's | |
| //// stored by InputManager.m_Layouts and in s_Layouts; we can't make it a plain array) | |
| public List<LayoutMatcher> layoutMatchers; | |
| public void Allocate() | |
| { | |
| layoutTypes = new Dictionary<InternedString, Type>(); | |
| layoutStrings = new Dictionary<InternedString, string>(); | |
| layoutBuilders = new Dictionary<InternedString, Func<InputControlLayout>>(); | |
| baseLayoutTable = new Dictionary<InternedString, InternedString>(); | |
| layoutOverrides = new Dictionary<InternedString, InternedString[]>(); | |
| layoutOverrideNames = new HashSet<InternedString>(); | |
| layoutMatchers = new List<LayoutMatcher>(); | |
| precompiledLayouts = new Dictionary<InternedString, PrecompiledLayout>(); | |
| } | |
| public InternedString TryFindLayoutForType(Type layoutType) | |
| { | |
| foreach (var entry in layoutTypes) | |
| if (entry.Value == layoutType) | |
| return entry.Key; | |
| return new InternedString(); | |
| } | |
| public InternedString TryFindMatchingLayout(InputDeviceDescription deviceDescription) | |
| { | |
| var highestScore = 0f; | |
| var highestScoringLayout = new InternedString(); | |
| var layoutMatcherCount = layoutMatchers.Count; | |
| for (var i = 0; i < layoutMatcherCount; ++i) | |
| { | |
| var matcher = layoutMatchers[i].deviceMatcher; | |
| var score = matcher.MatchPercentage(deviceDescription); | |
| // We want auto-generated layouts to take a backseat compared to manually created | |
| // layouts. We do this by boosting the score of every layout that isn't coming from | |
| // a layout builder. | |
| if (score > 0 && !layoutBuilders.ContainsKey(layoutMatchers[i].layoutName)) | |
| score += kBaseScoreForNonGeneratedLayouts; | |
| if (score > highestScore) | |
| { | |
| highestScore = score; | |
| highestScoringLayout = layoutMatchers[i].layoutName; | |
| } | |
| } | |
| return highestScoringLayout; | |
| } | |
| public bool HasLayout(InternedString name) | |
| { | |
| return layoutTypes.ContainsKey(name) || layoutStrings.ContainsKey(name) || | |
| layoutBuilders.ContainsKey(name); | |
| } | |
| private InputControlLayout TryLoadLayoutInternal(InternedString name) | |
| { | |
| // See if we have a string layout for it. These | |
| // always take precedence over ones from type so that we can | |
| // override what's in the code using data. | |
| if (layoutStrings.TryGetValue(name, out var json)) | |
| return FromJson(json); | |
| // No, but maybe we have a type layout for it. | |
| if (layoutTypes.TryGetValue(name, out var type)) | |
| return FromType(name, type); | |
| // Finally, check builders. Always the last ones to get a shot at | |
| // providing layouts. | |
| if (layoutBuilders.TryGetValue(name, out var builder)) | |
| { | |
| var layout = builder(); | |
| if (layout == null) | |
| throw new InvalidOperationException($"Layout builder '{name}' returned null when invoked"); | |
| return layout; | |
| } | |
| return null; | |
| } | |
| public InputControlLayout TryLoadLayout(InternedString name, Dictionary<InternedString, InputControlLayout> table = null) | |
| { | |
| // See if we have it cached. | |
| if (table != null && table.TryGetValue(name, out var layout)) | |
| return layout; | |
| layout = TryLoadLayoutInternal(name); | |
| if (layout != null) | |
| { | |
| layout.m_Name = name; | |
| if (layoutOverrideNames.Contains(name)) | |
| layout.isOverride = true; | |
| // If the layout extends another layout, we need to merge the | |
| // base layout into the final layout. | |
| // NOTE: We go through the baseLayoutTable here instead of looking at | |
| // the baseLayouts property so as to make this work for all types | |
| // of layouts (FromType() does not set the property, for example). | |
| var baseLayoutName = new InternedString(); | |
| if (!layout.isOverride && baseLayoutTable.TryGetValue(name, out baseLayoutName)) | |
| { | |
| Debug.Assert(!baseLayoutName.IsEmpty()); | |
| ////TODO: catch cycles | |
| var baseLayout = TryLoadLayout(baseLayoutName, table); | |
| if (baseLayout == null) | |
| throw new LayoutNotFoundException( | |
| $"Cannot find base layout '{baseLayoutName}' of layout '{name}'"); | |
| layout.MergeLayout(baseLayout); | |
| if (layout.m_BaseLayouts.length == 0) | |
| layout.m_BaseLayouts.Append(baseLayoutName); | |
| } | |
| // If there's overrides for the layout, apply them now. | |
| if (layoutOverrides.TryGetValue(name, out var overrides)) | |
| { | |
| for (var i = 0; i < overrides.Length; ++i) | |
| { | |
| var overrideName = overrides[i]; | |
| // NOTE: We do *NOT* pass `table` into TryLoadLayout here so that | |
| // the override we load will not get cached. The reason is that | |
| // we use MergeLayout which is destructive and thus should not | |
| // end up in the table. | |
| var overrideLayout = TryLoadLayout(overrideName); | |
| overrideLayout.MergeLayout(layout); | |
| // We're switching the layout we initially to the layout with | |
| // the overrides applied. Make sure we get rid of information here | |
| // from the override that we don't want to come through once the | |
| // override is applied. | |
| overrideLayout.m_BaseLayouts.Clear(); | |
| overrideLayout.isOverride = false; | |
| overrideLayout.isGenericTypeOfDevice = layout.isGenericTypeOfDevice; | |
| overrideLayout.m_Name = layout.name; | |
| overrideLayout.m_BaseLayouts = layout.m_BaseLayouts; | |
| layout = overrideLayout; | |
| layout.m_AppliedOverrides.Append(overrideName); | |
| } | |
| } | |
| if (table != null) | |
| table[name] = layout; | |
| } | |
| return layout; | |
| } | |
| public InternedString GetBaseLayoutName(InternedString layoutName) | |
| { | |
| if (baseLayoutTable.TryGetValue(layoutName, out var baseLayoutName)) | |
| return baseLayoutName; | |
| return default; | |
| } | |
| // Return name of layout at root of "extend" chain of given layout. | |
| public InternedString GetRootLayoutName(InternedString layoutName) | |
| { | |
| while (baseLayoutTable.TryGetValue(layoutName, out var baseLayout)) | |
| layoutName = baseLayout; | |
| return layoutName; | |
| } | |
| public bool ComputeDistanceInInheritanceHierarchy(InternedString firstLayout, InternedString secondLayout, out int distance) | |
| { | |
| distance = 0; | |
| // First try, assume secondLayout is based on firstLayout. | |
| var secondDistanceToFirst = 0; | |
| var current = secondLayout; | |
| while (!current.IsEmpty() && current != firstLayout) | |
| { | |
| current = GetBaseLayoutName(current); | |
| ++secondDistanceToFirst; | |
| } | |
| if (current == firstLayout) | |
| { | |
| distance = secondDistanceToFirst; | |
| return true; | |
| } | |
| // Second try, assume firstLayout is based on secondLayout. | |
| var firstDistanceToSecond = 0; | |
| current = firstLayout; | |
| while (!current.IsEmpty() && current != secondLayout) | |
| { | |
| current = GetBaseLayoutName(current); | |
| ++firstDistanceToSecond; | |
| } | |
| if (current == secondLayout) | |
| { | |
| distance = firstDistanceToSecond; | |
| return true; | |
| } | |
| return false; | |
| } | |
| public InternedString FindLayoutThatIntroducesControl(InputControl control, Cache cache) | |
| { | |
| // Find the topmost child control on the device. A device layout can only | |
| // add children that sit directly underneath it (e.g. "leftStick"). Children of children | |
| // are indirectly added by other layouts (e.g. "leftStick/x" which is added by "Stick"). | |
| // To determine which device contributes the control as a whole, we have to be looking | |
| // at the topmost child of the device. | |
| var topmostChild = control; | |
| while (topmostChild.parent != control.device) | |
| topmostChild = topmostChild.parent; | |
| // Find the layout in the device's base layout chain that first mentions the given control. | |
| // If we don't find it, we know it's first defined directly in the layout of the given device, | |
| // i.e. it's not an inherited control. | |
| var deviceLayoutName = control.device.m_Layout; | |
| var baseLayoutName = deviceLayoutName; | |
| while (baseLayoutTable.TryGetValue(baseLayoutName, out baseLayoutName)) | |
| { | |
| var layout = cache.FindOrLoadLayout(baseLayoutName); | |
| var controlItem = layout.FindControl(topmostChild.m_Name); | |
| if (controlItem != null) | |
| deviceLayoutName = baseLayoutName; | |
| } | |
| return deviceLayoutName; | |
| } | |
| // Get the type which will be instantiated for the given layout. | |
| // Returns null if no layout with the given name exists. | |
| public Type GetControlTypeForLayout(InternedString layoutName) | |
| { | |
| // Try layout strings. | |
| while (layoutStrings.ContainsKey(layoutName)) | |
| { | |
| if (baseLayoutTable.TryGetValue(layoutName, out var baseLayout)) | |
| { | |
| // Work our way up the inheritance chain. | |
| layoutName = baseLayout; | |
| } | |
| else | |
| { | |
| // Layout doesn't extend anything and ATM we don't support setting | |
| // types explicitly from JSON layouts. So has to be InputDevice. | |
| return typeof(InputDevice); | |
| } | |
| } | |
| // Try layout types. | |
| layoutTypes.TryGetValue(layoutName, out var result); | |
| return result; | |
| } | |
| // Return true if the given control layout has a value type whose values | |
| // can be assigned to variables of type valueType. | |
| public bool ValueTypeIsAssignableFrom(InternedString layoutName, Type valueType) | |
| { | |
| var controlType = GetControlTypeForLayout(layoutName); | |
| if (controlType == null) | |
| return false; | |
| var valueTypOfControl = | |
| TypeHelpers.GetGenericTypeArgumentFromHierarchy(controlType, typeof(InputControl<>), 0); | |
| if (valueTypOfControl == null) | |
| return false; | |
| return valueType.IsAssignableFrom(valueTypOfControl); | |
| } | |
| public bool IsGeneratedLayout(InternedString layout) | |
| { | |
| return layoutBuilders.ContainsKey(layout); | |
| } | |
| public IEnumerable<InternedString> GetBaseLayouts(InternedString layout, bool includeSelf = true) | |
| { | |
| if (includeSelf) | |
| yield return layout; | |
| while (baseLayoutTable.TryGetValue(layout, out layout)) | |
| yield return layout; | |
| } | |
| public bool IsBasedOn(InternedString parentLayout, InternedString childLayout) | |
| { | |
| var layout = childLayout; | |
| while (baseLayoutTable.TryGetValue(layout, out layout)) | |
| { | |
| if (layout == parentLayout) | |
| return true; | |
| } | |
| return false; | |
| } | |
| public void AddMatcher(InternedString layout, InputDeviceMatcher matcher) | |
| { | |
| // Ignore if already added. | |
| var layoutMatcherCount = layoutMatchers.Count; | |
| for (var i = 0; i < layoutMatcherCount; ++i) | |
| if (layoutMatchers[i].deviceMatcher == matcher) | |
| return; | |
| // Append. | |
| layoutMatchers.Add(new LayoutMatcher {layoutName = layout, deviceMatcher = matcher}); | |
| } | |
| } | |
| // This collection is owned and managed by InputManager. | |
| internal static Collection s_Layouts; | |
| public class LayoutNotFoundException : Exception | |
| { | |
| public string layout { get; } | |
| public LayoutNotFoundException() | |
| { | |
| } | |
| public LayoutNotFoundException(string name, string message) | |
| : base(message) | |
| { | |
| layout = name; | |
| } | |
| public LayoutNotFoundException(string name) | |
| : base($"Cannot find control layout '{name}'") | |
| { | |
| layout = name; | |
| } | |
| public LayoutNotFoundException(string message, Exception innerException) : | |
| base(message, innerException) | |
| { | |
| } | |
| protected LayoutNotFoundException(SerializationInfo info, | |
| StreamingContext context) : base(info, context) | |
| { | |
| } | |
| } | |
| // Constructs InputControlLayout instances and caches them. | |
| internal struct Cache | |
| { | |
| public Dictionary<InternedString, InputControlLayout> table; | |
| public void Clear() | |
| { | |
| table = null; | |
| } | |
| public InputControlLayout FindOrLoadLayout(string name, bool throwIfNotFound = true) | |
| { | |
| var internedName = new InternedString(name); | |
| if (table == null) | |
| table = new Dictionary<InternedString, InputControlLayout>(); | |
| var layout = s_Layouts.TryLoadLayout(internedName, table); | |
| if (layout != null) | |
| return layout; | |
| // Nothing. | |
| if (throwIfNotFound) | |
| throw new LayoutNotFoundException(name); | |
| return null; | |
| } | |
| } | |
| internal static Cache s_CacheInstance; | |
| internal static int s_CacheInstanceRef; | |
| // Constructing InputControlLayouts is very costly as it tends to involve lots of reflection and | |
| // piecing data together. Thus, wherever possible, we want to keep layouts around for as long as | |
| // we need them yet at the same time not keep them needlessly around while we don't. | |
| // | |
| // This property makes a cache of layouts available globally yet implements a resource acquisition | |
| // based pattern to make sure we keep the cache alive only within specific execution scopes. | |
| internal static ref Cache cache | |
| { | |
| get | |
| { | |
| Debug.Assert(s_CacheInstanceRef > 0, "Must hold an instance reference"); | |
| return ref s_CacheInstance; | |
| } | |
| } | |
| internal static CacheRefInstance CacheRef() | |
| { | |
| ++s_CacheInstanceRef; | |
| return new CacheRefInstance {valid = true}; | |
| } | |
| internal struct CacheRefInstance : IDisposable | |
| { | |
| public bool valid; // Make sure we can distinguish default-initialized instances. | |
| public void Dispose() | |
| { | |
| if (!valid) | |
| return; | |
| --s_CacheInstanceRef; | |
| if (s_CacheInstanceRef <= 0) | |
| { | |
| s_CacheInstance = default; | |
| s_CacheInstanceRef = 0; | |
| } | |
| valid = false; | |
| } | |
| } | |
| } | |
| } | |