2324 lines
100 KiB
C#
2324 lines
100 KiB
C#
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;
|
|
}
|
|
|
|
[Flags]
|
|
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;
|
|
|
|
[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();
|
|
}
|
|
|
|
[Serializable]
|
|
internal struct LayoutJsonNameAndDescriptorOnly
|
|
{
|
|
public string name;
|
|
public string extend;
|
|
public string[] extendMultiple;
|
|
public InputDeviceMatcher.MatcherJson device;
|
|
}
|
|
|
|
[Serializable]
|
|
private struct LayoutJson
|
|
{
|
|
// Disable warnings that these fields are never assigned to. They are set
|
|
// by JsonUtility.
|
|
#pragma warning disable 0649
|
|
// 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
|
|
#pragma warning restore 0649
|
|
|
|
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.
|
|
[Serializable]
|
|
private class ControlItemJson
|
|
{
|
|
// Disable warnings that these fields are never assigned to. They are set
|
|
// by JsonUtility.
|
|
#pragma warning disable 0649
|
|
// 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
|
|
#pragma warning restore 0649
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|