using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
////REVIEW: some of the stuff here is really low-level; should we move it into a separate static class inside of .LowLevel?
namespace UnityEngine.InputSystem
{
///
/// Various extension methods for . Mostly low-level routines.
///
public static class InputControlExtensions
{
///
/// Find a control of the given type in control hierarchy of .
///
/// Control whose parents to inspect.
/// Type of control to look for. Actual control type can be
/// subtype of this.
/// The found control of type which may be either
/// itself or one of its parents. If no such control was found,
/// returns null.
/// is null.
public static TControl FindInParentChain(this InputControl control)
where TControl : InputControl
{
if (control == null)
throw new ArgumentNullException(nameof(control));
for (var parent = control; parent != null; parent = parent.parent)
if (parent is TControl parentOfType)
return parentOfType;
return null;
}
////REVIEW: This ist too high up in the class hierarchy; can be applied to any kind of control without it being readily apparent what exactly it means
///
/// Check whether the given control is considered pressed according to the button press threshold.
///
/// Control to check.
/// Optional custom button press point. If not supplied,
/// is used.
/// True if the actuation of the given control is high enough for it to be considered pressed.
/// is null.
///
/// This method checks the actuation level of the control as does. For s
/// and other float value controls, this will effectively check whether the float value of the control exceeds the button
/// point threshold. Note that if the control is an axis that can be both positive and negative, the press threshold works in
/// both directions, i.e. it can be crossed both in the positive direction and in the negative direction.
///
///
///
///
public static bool IsPressed(this InputControl control, float buttonPressPoint = 0)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (Mathf.Approximately(0, buttonPressPoint))
{
if (control is ButtonControl button)
buttonPressPoint = button.pressPointOrDefault;
else
buttonPressPoint = ButtonControl.s_GlobalDefaultButtonPressPoint;
}
return control.IsActuated(buttonPressPoint);
}
///
/// Return true if the given control is actuated.
///
///
/// Magnitude threshold that the control must match or exceed to be considered actuated.
/// An exception to this is the default value of zero. If threshold is zero, the control must have a magnitude
/// greater than zero.
///
///
/// Actuation is defined as a control having a magnitude (
/// greater than zero or, if the control does not support magnitudes, has been moved from its default
/// state.
///
/// In practice, this means that when actuated, a control will produce a value other than its default
/// value.
///
public static bool IsActuated(this InputControl control, float threshold = 0)
{
// First perform cheap memory check. If we're in default state, we don't
// need to invoke virtuals on the control.
if (control.CheckStateIsAtDefault())
return false;
// Check magnitude of actuation.
var magnitude = control.EvaluateMagnitude();
if (magnitude < 0)
{
// We know the control is not in default state but we also know it doesn't support
// magnitude. So, all we can say is that it is actuated. Not how much it is actuated.
//
// If we're looking for a specific threshold here, consider the control to always
// be under. But if not, consider it actuated "by virtue of not being in default state".
if (Mathf.Approximately(threshold, 0))
return true;
return false;
}
if (Mathf.Approximately(threshold, 0))
return magnitude > 0;
return magnitude >= threshold;
}
///
/// Read the current value of the control and return it as an object.
///
///
///
/// This method allocates GC memory and thus may cause garbage collection when used during gameplay.
///
/// Use to read values generically without having to know the
/// specific value type of a control.
///
///
///
public static unsafe object ReadValueAsObject(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
return control.ReadValueFromStateAsObject(control.currentStatePtr);
}
///
/// Read the current, processed value of the control and store it into the given memory buffer.
///
/// Buffer to store value in. Note that the value is not stored with the offset
/// found in of the control's . It will
/// be stored directly at the given address.
/// Size of the memory available at in bytes. Has to be
/// at least . If the size is smaller, nothing will be written to the buffer.
///
///
///
public static unsafe void ReadValueIntoBuffer(this InputControl control, void* buffer, int bufferSize)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
control.ReadValueFromStateIntoBuffer(control.currentStatePtr, buffer, bufferSize);
}
///
/// Read the control's default value and return it as an object.
///
/// Control to read default value from.
///
/// is null.
///
/// This method allocates GC memory and should thus not be used during normal gameplay.
///
///
///
public static unsafe object ReadDefaultValueAsObject(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
return control.ReadValueFromStateAsObject(control.defaultStatePtr);
}
///
/// Read the value for the given control from the given event.
///
/// Control to read value for.
/// Event to read value from. Must be a or .
/// Type of value to read.
/// is null.
/// is not a or .
/// The value for the given control as read out from the given event or default(TValue) if the given
/// event does not contain a value for the control (e.g. if it is a not containing the relevant
/// portion of the device's state memory).
public static TValue ReadValueFromEvent(this InputControl control, InputEventPtr inputEvent)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!ReadValueFromEvent(control, inputEvent, out var value))
return default;
return value;
}
///
/// Check if the given event contains a value for the given control and if so, read the value.
///
/// Control to read value for.
/// Input event. This must be a or .
/// Note that in the case of a , the control may not actually be part of the event. In this
/// case, the method returns false and stores default(TValue) in .
/// Variable that receives the control value.
/// Type of value to read.
/// True if the value has been successfully read from the event, false otherwise.
/// is null.
/// is not a or .
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#")]
public static unsafe bool ReadValueFromEvent(this InputControl control, InputEventPtr inputEvent, out TValue value)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var statePtr = control.GetStatePtrFromStateEvent(inputEvent);
if (statePtr == null)
{
value = control.ReadDefaultValue();
return false;
}
value = control.ReadValueFromState(statePtr);
return true;
}
///
/// Read the value of from the given without having to
/// know the specific value type of the control.
///
/// Control to read the value for.
/// An or to read the value from.
/// The current value for the control or null if the control's value is not included
/// in the event.
///
public static unsafe object ReadValueFromEventAsObject(this InputControl control, InputEventPtr inputEvent)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var statePtr = control.GetStatePtrFromStateEvent(inputEvent);
if (statePtr == null)
return control.ReadDefaultValueAsObject();
return control.ReadValueFromStateAsObject(statePtr);
}
public static TValue ReadUnprocessedValueFromEvent(this InputControl control, InputEventPtr eventPtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var result = default(TValue);
control.ReadUnprocessedValueFromEvent(eventPtr, out result);
return result;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#")]
public static unsafe bool ReadUnprocessedValueFromEvent(this InputControl control, InputEventPtr inputEvent, out TValue value)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var statePtr = control.GetStatePtrFromStateEvent(inputEvent);
if (statePtr == null)
{
value = control.ReadDefaultValue();
return false;
}
value = control.ReadUnprocessedValueFromState(statePtr);
return true;
}
////REVIEW: this has the opposite argument order of WriteValueFromObjectIntoState; fix!
public static unsafe void WriteValueFromObjectIntoEvent(this InputControl control, InputEventPtr eventPtr, object value)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
var statePtr = control.GetStatePtrFromStateEvent(eventPtr);
if (statePtr == null)
return;
control.WriteValueFromObjectIntoState(value, statePtr);
}
///
/// Write the control's current value into .
///
/// Control to read the current value from and to store state for in .
/// State to receive the control's value in its respective .
/// is null or is null.
///
/// This method is equivalent to except that one does
/// not have to know the value type of the given control.
///
/// The control does not support writing. This is the case, for
/// example, that compute values (such as the magnitude of a vector).
///
public static unsafe void WriteValueIntoState(this InputControl control, void* statePtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
var valueSize = control.valueSizeInBytes;
var valuePtr = UnsafeUtility.Malloc(valueSize, 8, Allocator.Temp);
try
{
control.ReadValueFromStateIntoBuffer(control.currentStatePtr, valuePtr, valueSize);
control.WriteValueFromBufferIntoState(valuePtr, valueSize, statePtr);
}
finally
{
UnsafeUtility.Free(valuePtr, Allocator.Temp);
}
}
public static unsafe void WriteValueIntoState(this InputControl control, TValue value, void* statePtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!(control is InputControl controlOfType))
throw new ArgumentException(
$"Expecting control of type '{typeof(TValue).Name}' but got '{control.GetType().Name}'");
controlOfType.WriteValueIntoState(value, statePtr);
}
public static unsafe void WriteValueIntoState(this InputControl control, TValue value, void* statePtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
var valuePtr = UnsafeUtility.AddressOf(ref value);
var valueSize = UnsafeUtility.SizeOf();
control.WriteValueFromBufferIntoState(valuePtr, valueSize, statePtr);
}
public static unsafe void WriteValueIntoState(this InputControl control, void* statePtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
control.WriteValueIntoState(control.ReadValue(), statePtr);
}
///
///
///
///
/// Value for to write into .
///
/// is null.
/// Control's value does not fit within the memory of .
/// does not support writing.
public static unsafe void WriteValueIntoState(this InputControl control, TValue value, ref TState state)
where TValue : struct
where TState : struct, IInputStateTypeInfo
{
if (control == null)
throw new ArgumentNullException(nameof(control));
// Make sure the control's state actually fits within the given state.
var sizeOfState = UnsafeUtility.SizeOf();
if (control.stateOffsetRelativeToDeviceRoot + control.m_StateBlock.alignedSizeInBytes >= sizeOfState)
throw new ArgumentException(
$"Control {control.path} with offset {control.stateOffsetRelativeToDeviceRoot} and size of {control.m_StateBlock.sizeInBits} bits is out of bounds for state of type {typeof(TState).Name} with size {sizeOfState}",
nameof(state));
// Write value.
var statePtr = (byte*)UnsafeUtility.AddressOf(ref state);
control.WriteValueIntoState(value, statePtr);
}
public static void WriteValueIntoEvent(this InputControl control, TValue value, InputEventPtr eventPtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
if (!(control is InputControl controlOfType))
throw new ArgumentException(
$"Expecting control of type '{typeof(TValue).Name}' but got '{control.GetType().Name}'");
controlOfType.WriteValueIntoEvent(value, eventPtr);
}
public static unsafe void WriteValueIntoEvent(this InputControl control, TValue value, InputEventPtr eventPtr)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
var statePtr = control.GetStatePtrFromStateEvent(eventPtr);
if (statePtr == null)
return;
control.WriteValueIntoState(value, statePtr);
}
///
/// Copy the state of the device to the given memory buffer.
///
/// An input device.
/// Buffer to copy the state of the device to.
/// Size of in bytes.
/// is less than or equal to 0.
/// is null.
///
/// The method will copy however much fits into the given buffer. This means that if the state of the device
/// is larger than what fits into the buffer, not all of the device's state is copied.
///
///
public static unsafe void CopyState(this InputDevice device, void* buffer, int bufferSizeInBytes)
{
if (device == null)
throw new ArgumentNullException(nameof(device));
if (bufferSizeInBytes <= 0)
throw new ArgumentException("bufferSizeInBytes must be positive", nameof(bufferSizeInBytes));
var stateBlock = device.m_StateBlock;
var sizeToCopy = Math.Min(bufferSizeInBytes, stateBlock.alignedSizeInBytes);
UnsafeUtility.MemCpy(buffer, (byte*)device.currentStatePtr + stateBlock.byteOffset, sizeToCopy);
}
///
/// Copy the state of the device to the given struct.
///
/// An input device.
/// Struct to copy the state of the device into.
/// A state struct type such as .
/// The state format of does not match
/// the state form of .
/// is null.
///
/// This method will copy memory verbatim into the memory of the given struct. It will copy whatever
/// memory of the device fits into the given struct.
///
///
public static unsafe void CopyState(this InputDevice device, out TState state)
where TState : struct, IInputStateTypeInfo
{
if (device == null)
throw new ArgumentNullException(nameof(device));
state = default;
if (device.stateBlock.format != state.format)
throw new ArgumentException(
$"Struct '{typeof(TState).Name}' has state format '{state.format}' which doesn't match device '{device}' with state format '{device.stateBlock.format}'",
nameof(TState));
var stateSize = UnsafeUtility.SizeOf();
var statePtr = UnsafeUtility.AddressOf(ref state);
device.CopyState(statePtr, stateSize);
}
///
/// Check whether the memory of the given control is in its default state.
///
/// An input control on a device that's been added to the system (see ).
/// True if the state memory of the given control corresponds to the control's default.
/// is null.
///
/// This method is a cheaper check than actually reading out the value from the control and checking whether it
/// is the same value as the default value. The method bypasses all value reading and simply performs a trivial
/// memory comparison of the control's current state memory to the default state memory stored for the control.
///
/// Note that the default state for the memory of a control does not necessary need to be all zeroes. For example,
/// a stick axis may be stored as an unsigned 8-bit value with the memory state corresponding to a 0 value being 127.
///
///
public static unsafe bool CheckStateIsAtDefault(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
return CheckStateIsAtDefault(control, control.currentStatePtr);
}
///
/// Check if the given state corresponds to the default state of the control.
///
/// Control to check the state for in .
/// Pointer to a state buffer containing the for .
/// If not null, only bits set to false (!) in the buffer will be taken into account. This can be used
/// to mask out noise, i.e. every bit that is set in the mask is considered to represent noise.
/// True if the control/device is in its default state.
///
/// Note that default does not equate all zeroes. Stick axes, for example, that are stored as unsigned byte
/// values will have their resting position at 127 and not at 0. This is why we explicitly store default
/// state in a memory buffer instead of assuming zeroes.
///
///
public static unsafe bool CheckStateIsAtDefault(this InputControl control, void* statePtr, void* maskPtr = null)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CompareState(statePtr, control.defaultStatePtr, maskPtr);
}
public static unsafe bool CheckStateIsAtDefaultIgnoringNoise(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
return control.CheckStateIsAtDefaultIgnoringNoise(control.currentStatePtr);
}
///
/// Check if the given state corresponds to the default state of the control or has different state only
/// for parts marked as noise.
///
/// Control to check the state for in .
/// Pointer to a state buffer containing the for .
/// True if the control/device is in its default state (ignoring any bits marked as noise).
///
/// Note that default does not equate all zeroes. Stick axes, for example, that are stored as unsigned byte
/// values will have their resting position at 127 and not at 0. This is why we explicitly store default
/// state in a memory buffer instead of assuming zeroes.
///
///
///
///
public static unsafe bool CheckStateIsAtDefaultIgnoringNoise(this InputControl control, void* statePtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CheckStateIsAtDefault(statePtr, InputStateBuffers.s_NoiseMaskBuffer);
}
///
/// Compare the control's current state to the state stored in .
///
/// State memory containing the control's .
/// True if
///
///
/// This method ignores noise
///
/// This method will not actually read values but will instead compare state directly as it is stored
/// in memory. is not invoked and thus processors will
/// not be run.
///
public static unsafe bool CompareStateIgnoringNoise(this InputControl control, void* statePtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CompareState(control.currentStatePtr, statePtr, control.noiseMaskPtr);
}
///
/// Compare the control's stored state in to .
///
/// Memory containing the control's .
/// Memory containing the control's
/// Optional mask. If supplied, it will be used to mask the comparison between
/// and such that any bit not set in the
/// mask will be ignored even if different between the two states. This can be used, for example, to ignore
/// noise in the state ().
/// True if the state is equivalent in both memory buffers.
///
/// Unlike , this method only compares raw memory state. If used on a stick, for example,
/// it may mean that this method returns false for two stick values that would compare equal using
/// (e.g. if both stick values fall below the deadzone).
///
///
public static unsafe bool CompareState(this InputControl control, void* firstStatePtr, void* secondStatePtr, void* maskPtr = null)
{
////REVIEW: for compound controls, do we want to go check leaves so as to not pick up on non-control noise in the state?
//// e.g. from HID input reports; or should we just leave that to maskPtr?
var firstPtr = (byte*)firstStatePtr + (int)control.m_StateBlock.byteOffset;
var secondPtr = (byte*)secondStatePtr + (int)control.m_StateBlock.byteOffset;
var mask = maskPtr != null ? (byte*)maskPtr + (int)control.m_StateBlock.byteOffset : null;
if (control.m_StateBlock.sizeInBits == 1)
{
// If we have a mask and the bit is set in the mask, the control is to be ignored
// and thus we consider it at default value.
if (mask != null && MemoryHelpers.ReadSingleBit(mask, control.m_StateBlock.bitOffset))
return true;
return MemoryHelpers.ReadSingleBit(secondPtr, control.m_StateBlock.bitOffset) ==
MemoryHelpers.ReadSingleBit(firstPtr, control.m_StateBlock.bitOffset);
}
return MemoryHelpers.MemCmpBitRegion(firstPtr, secondPtr,
control.m_StateBlock.bitOffset, control.m_StateBlock.sizeInBits, mask);
}
public static unsafe bool CompareState(this InputControl control, void* statePtr, void* maskPtr = null)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CompareState(control.currentStatePtr, statePtr, maskPtr);
}
///
/// Return true if the current value of is different to the one found
/// in .
///
/// Control whose state to compare to what is stored in .
/// A block of input state memory containing the
/// of
/// is null or
/// is null.
/// True if the value of stored in is different
/// compared to what of the control returns.
public static unsafe bool HasValueChangeInState(this InputControl control, void* statePtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (statePtr == null)
throw new ArgumentNullException(nameof(statePtr));
return control.CompareValue(control.currentStatePtr, statePtr);
}
///
/// Return true if has a different value (from its current one) in
/// .
///
/// An input control.
/// An input event. Must be a or .
/// True if contains a value for that is different
/// from the control's current value (see ).
/// is null -or- is a null pointer (see ).
/// is not a or .
public static unsafe bool HasValueChangeInEvent(this InputControl control, InputEventPtr eventPtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
var statePtr = control.GetStatePtrFromStateEvent(eventPtr);
if (statePtr == null)
return false;
return control.CompareValue(control.currentStatePtr, statePtr);
}
///
/// Given a or , return the raw memory pointer that can
/// be used, for example, with to read out the value of
/// contained in the event.
///
/// Control to access state for in the given state event.
/// A or containing input state.
/// A pointer that can be used with methods such as or null
/// if does not contain state for the given .
/// is null -or- is invalid.
/// is not a or .
///
/// Note that the given state event must have the same state format (see ) as the device
/// to which belongs. If this is not the case, the method will invariably return null.
///
/// In practice, this means that the method cannot be used with touch events or, in general, with events sent to devices
/// that implement (which does) except if the event
/// is in the same state format as the device. Touch events will generally be sent as state events containing only the
/// state of a single , not the state of the entire .
///
public static unsafe void* GetStatePtrFromStateEvent(this InputControl control, InputEventPtr eventPtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
return GetStatePtrFromStateEventUnchecked(control, eventPtr, eventPtr.type);
}
internal static unsafe void* GetStatePtrFromStateEventUnchecked(this InputControl control, InputEventPtr eventPtr, FourCC eventType)
{
uint stateOffset;
FourCC stateFormat;
uint stateSizeInBytes;
void* statePtr;
if (eventType == StateEvent.Type)
{
var stateEvent = StateEvent.FromUnchecked(eventPtr);
stateOffset = 0;
stateFormat = stateEvent->stateFormat;
stateSizeInBytes = stateEvent->stateSizeInBytes;
statePtr = stateEvent->state;
}
else if (eventType == DeltaStateEvent.Type)
{
var deltaEvent = DeltaStateEvent.FromUnchecked(eventPtr);
// If it's a delta event, we need to subtract the delta state offset if it's not set to the root of the device
stateOffset = deltaEvent->stateOffset;
stateFormat = deltaEvent->stateFormat;
stateSizeInBytes = deltaEvent->deltaStateSizeInBytes;
statePtr = deltaEvent->deltaState;
}
else
{
throw new ArgumentException($"Event must be a StateEvent or DeltaStateEvent but is a {eventType} instead",
nameof(eventPtr));
}
// Make sure we have a state event compatible with our device. The event doesn't
// have to be specifically for our device (we don't require device IDs to match) but
// the formats have to match and the size must be within range of what we're trying
// to read.
var device = control.device;
if (stateFormat != device.m_StateBlock.format)
{
// If the device is an IInputStateCallbackReceiver, there's a chance it actually recognizes
// the state format in the event and can correlate it to the state as found on the device.
if (!device.hasStateCallbacks ||
!((IInputStateCallbackReceiver)device).GetStateOffsetForEvent(control, eventPtr, ref stateOffset))
return null;
}
// Once a device has been added, global state buffer offsets are baked into control hierarchies.
// We need to unsubtract those offsets here.
// NOTE: If the given device has not actually been added to the system, the offset is simply 0
// and this is a harmless NOP.
stateOffset += device.m_StateBlock.byteOffset;
// Return null if state is out of range.
ref var controlStateBlock = ref control.m_StateBlock;
var controlOffset = (int)controlStateBlock.effectiveByteOffset - stateOffset;
if (controlOffset < 0 || controlOffset + controlStateBlock.alignedSizeInBytes > stateSizeInBytes)
return null;
return (byte*)statePtr - (int)stateOffset;
}
///
/// Writes the default state of into .
///
/// A control whose default state to write.
/// A valid pointer to either a or .
/// is null -or- contains
/// a null pointer.
/// is not a or .
/// True if the default state for was written to , false if the
/// given state or delta state event did not include memory for the given control.
///
/// Note that the default state of a control does not necessarily need to correspond to zero-initialized memory. For example, if
/// an axis control yields a range of [-1..1] and is stored as a signed 8-bit value, the default state will be 127, not 0.
///
///
///
/// // Reset the left gamepad stick to its default state (which results in a
/// // value of (0,0).
/// using (StateEvent.From(Gamepad.all[0], out var eventPtr))
/// {
/// Gamepad.all[0].leftStick.ResetToDefaultStateInEvent(eventPtr);
/// InputSystem.QueueEvent(eventPtr);
/// }
///
///
///
///
public static unsafe bool ResetToDefaultStateInEvent(this InputControl control, InputEventPtr eventPtr)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr));
var eventType = eventPtr.type;
if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
throw new ArgumentException("Given event is not a StateEvent or a DeltaStateEvent", nameof(eventPtr));
var statePtr = (byte*)control.GetStatePtrFromStateEvent(eventPtr);
if (statePtr == null)
return false;
var defaultStatePtr = (byte*)control.defaultStatePtr;
ref var stateBlock = ref control.m_StateBlock;
var offset = stateBlock.byteOffset;
MemoryHelpers.MemCpyBitRegion(statePtr + offset, defaultStatePtr + offset, stateBlock.bitOffset, stateBlock.sizeInBits);
return true;
}
///
/// Queue a value change on the given which will be processed and take effect
/// in the next input update.
///
/// Control to change the value of.
/// New value for the control.
/// Optional time at which the value change should take effect. If set, this will become
/// the of the queued event. If the time is in the future and the update mode is
/// set to , the event will not be processed until
/// it falls within the time of an input update slice. Otherwise, the event will invariably be consumed in the
/// next input update (see ).
/// Type of value.
/// is null.
public static void QueueValueChange(this InputControl control, TValue value, double time = -1)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
////TODO: if it's not a bit-addressing control, send a delta state change only
using (StateEvent.From(control.device, out var eventPtr))
{
if (time >= 0)
eventPtr.time = time;
control.WriteValueIntoEvent(value, eventPtr);
InputSystem.QueueEvent(eventPtr);
}
}
///
/// Modify to write an accumulated value of the control
/// rather than the value currently found in the event.
///
/// Control to perform the accumulation on.
/// Memory containing the control's current state. See .
/// Event containing the new state about to be written to the device.
/// is null.
///
/// This method reads the current, unprocessed value of the control from
/// and then adds it to the value of the control found in .
///
/// Note that the method does nothing if a value for the control is not contained in .
/// This can be the case, for example, for s.
///
///
public static unsafe void AccumulateValueInEvent(this InputControl control, void* currentStatePtr, InputEventPtr newState)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!control.ReadUnprocessedValueFromEvent(newState, out var newValue))
return; // Value for the control not contained in the given event.
var oldValue = control.ReadUnprocessedValueFromState(currentStatePtr);
control.WriteValueIntoEvent(oldValue + newValue, newState);
}
internal static unsafe void AccumulateValueInEvent(this InputControl control, void* currentStatePtr, InputEventPtr newState)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!control.ReadUnprocessedValueFromEvent(newState, out var newValue))
return; // Value for the control not contained in the given event.
var oldDelta = control.ReadUnprocessedValueFromState(currentStatePtr);
control.WriteValueIntoEvent(oldDelta + newValue, newState);
}
public static void FindControlsRecursive(this InputControl parent, IList controls, Func predicate)
where TControl : InputControl
{
if (parent == null)
throw new ArgumentNullException(nameof(parent));
if (controls == null)
throw new ArgumentNullException(nameof(controls));
if (predicate == null)
throw new ArgumentNullException(nameof(predicate));
if (parent is TControl parentAsTControl && predicate(parentAsTControl))
controls.Add(parentAsTControl);
var children = parent.children;
var childCount = children.Count;
for (var i = 0; i < childCount; ++i)
{
var child = parent.children[i];
FindControlsRecursive(child, controls, predicate);
}
}
internal static string BuildPath(this InputControl control, string deviceLayout, StringBuilder builder = null)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (string.IsNullOrEmpty(deviceLayout))
throw new ArgumentNullException(nameof(deviceLayout));
if (builder == null)
builder = new StringBuilder();
var device = control.device;
builder.Append('<');
builder.Append(deviceLayout.Escape("\\>", "\\>"));
builder.Append('>');
// Add usages of device, if any.
var deviceUsages = device.usages;
for (var i = 0; i < deviceUsages.Count; ++i)
{
builder.Append('{');
builder.Append(deviceUsages[i].ToString().Escape("\\}", "\\}"));
builder.Append('}');
}
builder.Append(InputControlPath.Separator);
// If any of the components contains a backslash, double it up as in control paths,
// these serve as escape characters.
var devicePath = device.path.Replace("\\", "\\\\");
var controlPath = control.path.Replace("\\", "\\\\");
builder.Append(controlPath, devicePath.Length + 1, controlPath.Length - devicePath.Length - 1);
return builder.ToString();
}
///
/// Flags that control which controls are returned by .
///
[Flags]
public enum Enumerate
{
///
/// Ignore controls whose value is at default (see ).
///
IgnoreControlsInDefaultState = 1 << 0,
///
/// Ignore controls whose value is the same as their current value (see ).
///
IgnoreControlsInCurrentState = 1 << 1,
///
/// Include controls that are and/or use state from other other controls (see
/// ). These are excluded by default.
///
IncludeSyntheticControls = 1 << 2,
///
/// Include noisy controls (see ). These are excluded by default.
///
IncludeNoisyControls = 1 << 3,
///
/// For any leaf control returned by the enumeration, also return all the parent controls (see )
/// in turn (but not the root itself).
///
IncludeNonLeafControls = 1 << 4,
}
///
/// Go through the controls that have values in , apply the given filters, and return each
/// control one by one.
///
/// An input event. Must be a or .
/// Filter settings that determine which controls to return.
/// Input device from which to enumerate controls. If this is null, then the
/// from the given will be used to locate the device via . If the device
/// cannot be found, an exception will be thrown. Note that type of device must match the state stored in the given event.
/// If not zero, minimum actuation threshold (see ) that
/// a control must reach (as per value in the given ) in order for it to be included in the enumeration.
/// An enumerator for the controls with values in .
/// is a null pointer (see ).
/// is not a and not a -or-
/// is null and no device was found with a matching that of
/// found in .
///
/// This method is much more efficient than manually iterating over the controls of and locating
/// the ones that have changed in . See for details.
///
/// This method will not allocate GC memory and can safely be used with foreach loops.
///
///
///
///
///
public static InputEventControlCollection EnumerateControls(this InputEventPtr eventPtr, Enumerate flags, InputDevice device = null, float magnitudeThreshold = 0)
{
if (!eventPtr.valid)
throw new ArgumentNullException(nameof(eventPtr), "Given event pointer must not be null");
var eventType = eventPtr.type;
if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
throw new ArgumentException($"Event must be a StateEvent or DeltaStateEvent but is a {eventType} instead", nameof(eventPtr));
// Look up device from event, if no device was supplied.
if (device == null)
{
var deviceId = eventPtr.deviceId;
device = InputSystem.GetDeviceById(deviceId);
if (device == null)
throw new ArgumentException($"Cannot find device with ID {deviceId} referenced by event", nameof(eventPtr));
}
return new InputEventControlCollection { m_Device = device, m_EventPtr = eventPtr, m_Flags = flags, m_MagnitudeThreshold = magnitudeThreshold };
}
///
/// Go through all controls in the given that have changed value.
///
/// An input event. Must be a or .
/// Input device from which to enumerate controls. If this is null, then the
/// from the given will be used to locate the device via . If the device
/// cannot be found, an exception will be thrown. Note that type of device must match the state stored in the given event.
/// If not zero, minimum actuation threshold (see ) that
/// a control must reach (as per value in the given ) in order for it to be included in the enumeration.
/// An enumerator for the controls that have changed values in .
///
/// This method is a shorthand for calling with .
///
///
///
/// // Detect button presses.
/// InputSystem.onEvent +=
/// (eventPtr, device) =>
/// {
/// // Ignore anything that is not a state event.
/// var eventType = eventPtr.type;
/// if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
/// return;
///
/// // Find all changed controls actuated above the button press threshold.
/// foreach (var control in eventPtr.EnumerateChangedControls
/// (device: device, magnitudeThreshold: InputSystem.settings.defaultButtonPressThreshold))
/// if (control is ButtonControl button)
/// Debug.Log($"Button {button} was pressed");
/// }
///
///
///
///
///
///
public static InputEventControlCollection EnumerateChangedControls(this InputEventPtr eventPtr, InputDevice device = null, float magnitudeThreshold = 0)
{
return eventPtr.EnumerateControls
(Enumerate.IgnoreControlsInCurrentState, device, magnitudeThreshold);
}
///
/// Return true if the given has any
///
/// An event. Must be a or .
/// The threshold value that a button must be actuated by to be considered pressed.
/// Whether the method should only consider button controls.
///
/// is a null pointer.
/// is not a or -or-
/// the referenced by the in the event cannot be found.
///
///
public static bool HasButtonPress(this InputEventPtr eventPtr, float magnitude = -1, bool buttonControlsOnly = true)
{
return eventPtr.GetFirstButtonPressOrNull(magnitude, buttonControlsOnly) != null;
}
///
/// Get the first pressed button from the given event or null if the event doesn't contain a new button press.
///
/// An event. Must be a or .
/// The threshold value that a control must be actuated by (see
/// ) to be considered pressed. If not given, defaults to .
/// Whether the method should only consider s. Otherwise,
/// any that has an actuation (see ) equal to
/// or greater than the given will be considered a pressed button. This is 'true' by
/// default.
/// The control that was pressed.
/// is a null pointer.
/// The referenced by the in the event cannot
/// be found.
///
///
/// Buttons will be evaluated in the order that they appear in the devices layout i.e. the bit position of each control
/// in the devices state memory. For example, in the gamepad state, button north (bit position 4) will be evaluated before button
/// east (bit position 5), so if both buttons were pressed in the given event, button north would be returned.
/// Note that the function returns null if the is not a StateEvent or DeltaStateEvent.
public static InputControl GetFirstButtonPressOrNull(this InputEventPtr eventPtr, float magnitude = -1, bool buttonControlsOnly = true)
{
if (eventPtr.type != StateEvent.Type && eventPtr.type != DeltaStateEvent.Type)
return null;
if (magnitude < 0)
magnitude = InputSystem.settings.defaultButtonPressPoint;
foreach (var control in eventPtr.EnumerateControls(Enumerate.IgnoreControlsInDefaultState, magnitudeThreshold: magnitude))
{
if (buttonControlsOnly && !control.isButton)
continue;
return control;
}
return null;
}
///
/// Enumerate all pressed buttons in the given event.
///
/// An event. Must be a or .
/// The threshold value that a button must be actuated by to be considered pressed.
/// Whether the method should only consider button controls.
/// An enumerable collection containing all buttons that were pressed in the given event.
/// is a null pointer.
/// The referenced by the in the event cannot be found.
/// Returns an empty enumerable if the is not a or .
///
///
public static IEnumerable GetAllButtonPresses(this InputEventPtr eventPtr, float magnitude = -1, bool buttonControlsOnly = true)
{
if (eventPtr.type != StateEvent.Type && eventPtr.type != DeltaStateEvent.Type)
yield break;
if (magnitude < 0)
magnitude = InputSystem.settings.defaultButtonPressPoint;
foreach (var control in eventPtr.EnumerateControls(Enumerate.IgnoreControlsInDefaultState, magnitudeThreshold: magnitude))
{
if (buttonControlsOnly && !control.isButton)
continue;
yield return control;
}
}
///
/// Allows iterating over the controls referenced by an via .
///
///
///
public struct InputEventControlCollection : IEnumerable
{
internal InputDevice m_Device;
internal InputEventPtr m_EventPtr;
internal Enumerate m_Flags;
internal float m_MagnitudeThreshold;
///
/// The event being iterated over. A or .
///
public InputEventPtr eventPtr => m_EventPtr;
///
/// Enumerate the controls in the event.
///
/// An enumerator.
public InputEventControlEnumerator GetEnumerator()
{
return new InputEventControlEnumerator(m_EventPtr, m_Device, m_Flags, m_MagnitudeThreshold);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
///
/// Iterates over the controls in a or
/// while optionally applying certain filtering criteria.
///
///
/// One problem with state events (that is, and )
/// is that they contain raw blocks of memory which may contain state changes for arbitrary many
/// controls on a device at the same time. Locating individual controls and determining which have
/// changed state and how can thus be quite inefficient.
///
/// This helper aims to provide an easy and efficient way to iterate over controls relevant to a
/// given state event. Instead of iterating over the controls of a device looking for the ones
/// relevant to a given event, enumeration is done the opposite by efficiently searching through
/// the memory contained in an event and then mapping data found in the event back to controls
/// on a given device.
///
///
///
/// // Inefficient:
/// foreach (var control in device.allControls)
/// {
/// // Skip control if it is noisy, synthetic, or not a leaf control.
/// if (control.synthetic || control.noisy || control.children.Count > 0)
/// continue;
///
/// // Locate the control in the event.
/// var statePtr = eventPtr.GetStatePtrFromStateEvent(eventPtr);
/// if (statePtr == null)
/// continue; // Control not included in event.
///
/// // See if the control is actuated beyond a minimum threshold.
/// if (control.EvaluateMagnitude(statePtr) < 0.001f)
/// continue;
///
/// Debug.Log($"Found actuated control {control}");
/// }
///
/// // Much more efficient:
/// foreach (var control in eventPtr.EnumerateControls(
/// InputControlExtensions.Enumerate.IgnoreControlsInDefaultState,
/// device: device,
/// magnitudeThreshold: 0.001f))
/// {
/// Debug.Log($"Found actuated control {control}");
/// }
///
///
///
///
///
public unsafe struct InputEventControlEnumerator : IEnumerator
{
private Enumerate m_Flags;
private readonly InputDevice m_Device;
private readonly uint[] m_StateOffsetToControlIndex;
private readonly int m_StateOffsetToControlIndexLength;
private readonly InputControl[] m_AllControls;
private byte* m_DefaultState; // Already offset by device offset.
private byte* m_CurrentState;
private byte* m_NoiseMask; // Already offset by device offset.
private InputEventPtr m_EventPtr;
private InputControl m_CurrentControl;
private int m_CurrentIndexInStateOffsetToControlIndexMap;
private uint m_CurrentControlStateBitOffset;
private byte* m_EventState;
private uint m_CurrentBitOffset;
private uint m_EndBitOffset;
private float m_MagnitudeThreshold;
internal InputEventControlEnumerator(InputEventPtr eventPtr, InputDevice device, Enumerate flags, float magnitudeThreshold = 0)
{
Debug.Assert(eventPtr.valid, "eventPtr should be valid at this point");
Debug.Assert(device != null, "Need to have valid device at this point");
m_Device = device;
m_StateOffsetToControlIndex = device.m_StateOffsetToControlMap;
m_StateOffsetToControlIndexLength = m_StateOffsetToControlIndex.LengthSafe();
m_AllControls = device.m_ChildrenForEachControl;
m_EventPtr = eventPtr;
m_Flags = flags;
m_CurrentControl = null;
m_CurrentIndexInStateOffsetToControlIndexMap = default;
m_CurrentControlStateBitOffset = 0;
m_EventState = default;
m_CurrentBitOffset = default;
m_EndBitOffset = default;
m_MagnitudeThreshold = magnitudeThreshold;
if ((flags & Enumerate.IncludeNoisyControls) == 0)
m_NoiseMask = (byte*)device.noiseMaskPtr + device.m_StateBlock.byteOffset;
else
m_NoiseMask = default;
if ((flags & Enumerate.IgnoreControlsInDefaultState) != 0)
m_DefaultState = (byte*)device.defaultStatePtr + device.m_StateBlock.byteOffset;
else
m_DefaultState = default;
if ((flags & Enumerate.IgnoreControlsInCurrentState) != 0)
m_CurrentState = (byte*)device.currentStatePtr + device.m_StateBlock.byteOffset;
else
m_CurrentState = default;
Reset();
}
private bool CheckDefault(uint numBits)
{
return MemoryHelpers.MemCmpBitRegion(m_EventState, m_DefaultState, m_CurrentBitOffset, numBits, m_NoiseMask);
}
private bool CheckCurrent(uint numBits)
{
return MemoryHelpers.MemCmpBitRegion(m_EventState, m_CurrentState, m_CurrentBitOffset, numBits, m_NoiseMask);
}
public bool MoveNext()
{
if (!m_EventPtr.valid)
throw new ObjectDisposedException("Enumerator has already been disposed");
// If we are to include non-leaf controls and we have a current control, walk
// up the tree until we reach the device.
if (m_CurrentControl != null && (m_Flags & Enumerate.IncludeNonLeafControls) != 0)
{
var parent = m_CurrentControl.parent;
if (parent != m_Device)
{
m_CurrentControl = parent;
return true;
}
}
var ignoreDefault = m_DefaultState != null;
var ignoreCurrent = m_CurrentState != null;
// Search for the next control that matches our filter criteria.
while (true)
{
m_CurrentControl = null;
// If we are ignoring certain state values, try to skip over as much memory as we can.
if (ignoreCurrent || ignoreDefault)
{
// If we are not byte-aligned, search whatever bits are left in the current byte.
if ((m_CurrentBitOffset & 0x7) != 0)
{
var bitsLeftInByte = (m_CurrentBitOffset + 8) & 0x7;
if ((ignoreCurrent && CheckCurrent(bitsLeftInByte))
|| (ignoreDefault && CheckDefault(bitsLeftInByte)))
m_CurrentBitOffset += bitsLeftInByte;
}
// Search byte by byte.
while (m_CurrentBitOffset < m_EndBitOffset)
{
var byteOffset = m_CurrentBitOffset >> 3;
var eventByte = m_EventState[byteOffset];
var maskByte = m_NoiseMask != null ? m_NoiseMask[byteOffset] : 0xff;
if (ignoreCurrent)
{
var currentByte = m_CurrentState[byteOffset];
if ((currentByte & maskByte) == (eventByte & maskByte))
{
m_CurrentBitOffset += 8;
continue;
}
}
if (ignoreDefault)
{
var defaultByte = m_DefaultState[byteOffset];
if ((defaultByte & maskByte) == (eventByte & maskByte))
{
m_CurrentBitOffset += 8;
continue;
}
}
break;
}
}
// See if we've reached the end.
if (m_CurrentBitOffset >= m_EndBitOffset
|| m_CurrentIndexInStateOffsetToControlIndexMap >= m_StateOffsetToControlIndexLength) // No more controls.
return false;
// No, so find the control at the current bit offset.
for (;
m_CurrentIndexInStateOffsetToControlIndexMap < m_StateOffsetToControlIndexLength;
++m_CurrentIndexInStateOffsetToControlIndexMap)
{
InputDevice.DecodeStateOffsetToControlMapEntry(
m_StateOffsetToControlIndex[m_CurrentIndexInStateOffsetToControlIndexMap],
out var controlIndex,
out var controlBitOffset,
out var controlBitSize);
// If the control's bit region lies *before* the memory we're looking at,
// skip it.
if (controlBitOffset < m_CurrentControlStateBitOffset ||
m_CurrentBitOffset >= (controlBitOffset + controlBitSize - m_CurrentControlStateBitOffset))
continue;
// If the bit region we're looking at lies *before* the current control,
// keep searching through memory.
if ((controlBitOffset - m_CurrentControlStateBitOffset) >= m_CurrentBitOffset + 8)
{
// Jump to location of control.
m_CurrentBitOffset = controlBitOffset - m_CurrentControlStateBitOffset;
break;
}
// If the control's bit region runs past of what we actually have (may be the case both
// with delta events and normal state events), skip it.
if (controlBitOffset + controlBitSize - m_CurrentControlStateBitOffset > m_EndBitOffset)
continue;
// If the control is byte-aligned both in its start offset and its length,
// we have what we're looking for.
if ((controlBitOffset & 0x7) == 0 && (controlBitSize & 0x7) == 0)
{
m_CurrentControl = m_AllControls[controlIndex];
}
else
{
// Otherwise, we may need to check the bit region specifically for the control.
if ((ignoreCurrent && MemoryHelpers.MemCmpBitRegion(m_EventState, m_CurrentState, controlBitOffset - m_CurrentControlStateBitOffset, controlBitSize, m_NoiseMask))
|| (ignoreDefault && MemoryHelpers.MemCmpBitRegion(m_EventState, m_DefaultState, controlBitOffset - m_CurrentControlStateBitOffset, controlBitSize, m_NoiseMask)))
continue;
m_CurrentControl = m_AllControls[controlIndex];
}
if ((m_Flags & Enumerate.IncludeNoisyControls) == 0 && m_CurrentControl.noisy)
{
m_CurrentControl = null;
continue;
}
if ((m_Flags & Enumerate.IncludeSyntheticControls) == 0)
{
var controlHasSharedState = (m_CurrentControl.m_ControlFlags &
(InputControl.ControlFlags.UsesStateFromOtherControl |
InputControl.ControlFlags.IsSynthetic)) != 0;
// Filter out synthetic and useStateFrom controls.
if (controlHasSharedState)
{
m_CurrentControl = null;
continue;
}
}
++m_CurrentIndexInStateOffsetToControlIndexMap;
break;
}
if (m_CurrentControl != null)
{
// If we are the filter by magnitude, last check is to go let the control evaluate
// its magnitude based on the data in the event and if it's too low, keep searching.
if (m_MagnitudeThreshold != 0)
{
var statePtr = m_EventState - (m_CurrentControlStateBitOffset >> 3) - m_Device.m_StateBlock.byteOffset;
var magnitude = m_CurrentControl.EvaluateMagnitude(statePtr);
if (magnitude >= 0 && magnitude < m_MagnitudeThreshold)
continue;
}
return true;
}
}
}
public void Reset()
{
if (!m_EventPtr.valid)
throw new ObjectDisposedException("Enumerator has already been disposed");
var eventType = m_EventPtr.type;
FourCC stateFormat;
if (eventType == StateEvent.Type)
{
var stateEvent = StateEvent.FromUnchecked(m_EventPtr);
m_EventState = (byte*)stateEvent->state;
m_EndBitOffset = stateEvent->stateSizeInBytes * 8;
m_CurrentBitOffset = 0;
stateFormat = stateEvent->stateFormat;
}
else if (eventType == DeltaStateEvent.Type)
{
var deltaEvent = DeltaStateEvent.FromUnchecked(m_EventPtr);
m_EventState = (byte*)deltaEvent->deltaState - deltaEvent->stateOffset; // We access m_EventState as if it contains a full state event.
m_CurrentBitOffset = deltaEvent->stateOffset * 8;
m_EndBitOffset = m_CurrentBitOffset + deltaEvent->deltaStateSizeInBytes * 8;
stateFormat = deltaEvent->stateFormat;
}
else
{
throw new NotSupportedException($"Cannot iterate over controls in event of type '{eventType}'");
}
m_CurrentIndexInStateOffsetToControlIndexMap = 0;
m_CurrentControlStateBitOffset = 0;
m_CurrentControl = null;
// If the state format of the event does not match that of the device,
// we need to go through the IInputStateCallbackReceiver machinery to adapt.
if (stateFormat != m_Device.m_StateBlock.format)
{
var stateOffset = 0u;
if (m_Device.hasStateCallbacks &&
((IInputStateCallbackReceiver)m_Device).GetStateOffsetForEvent(null, m_EventPtr, ref stateOffset))
{
m_CurrentControlStateBitOffset = stateOffset * 8;
if (m_CurrentState != null)
m_CurrentState += stateOffset;
if (m_DefaultState != null)
m_DefaultState += stateOffset;
if (m_NoiseMask != null)
m_NoiseMask += stateOffset;
}
else
{
// https://fogbugz.unity3d.com/f/cases/1395648/
if (m_Device is Touchscreen && m_EventPtr.IsA() &&
StateEvent.FromUnchecked(m_EventPtr)->stateFormat == TouchState.Format)
{
// if GetStateOffsetForEvent(null, ...) return false on touchscreen it means that
// we don't have a free slot for incoming touch, so ignore it for now
}
else
throw new InvalidOperationException(
$"{eventType} event with state format {stateFormat} cannot be used with device '{m_Device}'");
}
}
// NOTE: We *could* run a CheckDefault() or even CheckCurrent() over the entire event here to rule
// it out entirely. However, we don't do so based on the assumption that *in general* this will
// only *add* time. Rationale:
//
// - We assume that it is very rare for devices to send events matching the state the device
// already has (i.e. *entire* event is just == current state).
// - We assume that it is less common than the opposite for devices to send StateEvents containing
// nothing but default state. This happens frequently for the keyboard but is very uncommon for mice,
// touchscreens, and gamepads (where the sticks will almost never be exactly at default).
// - We assume that for DeltaStateEvents it is in fact quite common to contain only default state but
// that since in most cases these will contain state for either a very small set of controls or even
// just a single one, the work we do in MoveNext somewhat closely matches that we'd here with a CheckXXX()
// call but that we'd add work to every DeltaStateEvent if we were to have the upfront comparison here.
}
public void Dispose()
{
m_EventPtr = default;
}
public InputControl Current => m_CurrentControl;
object IEnumerator.Current => Current;
}
// Undocumented APIs. Meant to be used only by auto-generated, precompiled layouts.
// These APIs exist solely to keep access to the various properties/fields internal
// and only allow their contents to be modified in a controlled manner.
#region Undocumented
public static ControlBuilder Setup(this InputControl control)
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (control.isSetupFinished)
throw new InvalidOperationException($"The setup of {control} cannot be modified; control is already in use");
return new ControlBuilder { control = control };
}
public static DeviceBuilder Setup(this InputDevice device, int controlCount, int usageCount, int aliasCount)
{
if (device == null)
throw new ArgumentNullException(nameof(device));
if (device.isSetupFinished)
throw new InvalidOperationException($"The setup of {device} cannot be modified; control is already in use");
if (controlCount < 1)
throw new ArgumentOutOfRangeException(nameof(controlCount));
if (usageCount < 0)
throw new ArgumentOutOfRangeException(nameof(usageCount));
if (aliasCount < 0)
throw new ArgumentOutOfRangeException(nameof(aliasCount));
device.m_Device = device;
device.m_ChildrenForEachControl = new InputControl[controlCount];
if (usageCount > 0)
{
device.m_UsagesForEachControl = new InternedString[usageCount];
device.m_UsageToControl = new InputControl[usageCount];
}
if (aliasCount > 0)
device.m_AliasesForEachControl = new InternedString[aliasCount];
return new DeviceBuilder { device = device };
}
public struct ControlBuilder
{
public InputControl control { get; internal set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder At(InputDevice device, int index)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (device == null)
throw new ArgumentNullException(nameof(device));
if (index < 0 || index >= device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(index));
#endif
device.m_ChildrenForEachControl[index] = control;
control.m_Device = device;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithParent(InputControl parent)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (parent == null)
throw new ArgumentNullException(nameof(parent));
if (parent == control)
throw new ArgumentException("Control cannot be its own parent", nameof(parent));
#endif
control.m_Parent = parent;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithName(string name)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
#endif
control.m_Name = new InternedString(name);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithDisplayName(string displayName)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(displayName))
throw new ArgumentNullException(nameof(displayName));
#endif
control.m_DisplayNameFromLayout = new InternedString(displayName);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithShortDisplayName(string shortDisplayName)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(shortDisplayName))
throw new ArgumentNullException(nameof(shortDisplayName));
#endif
control.m_ShortDisplayNameFromLayout = new InternedString(shortDisplayName);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithLayout(InternedString layout)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (layout.IsEmpty())
throw new ArgumentException("Layout name cannot be empty", nameof(layout));
#endif
control.m_Layout = layout;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithUsages(int startIndex, int count)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (startIndex < 0 || startIndex >= control.device.m_UsagesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0 || startIndex + count > control.device.m_UsagesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(count));
#endif
control.m_UsageStartIndex = startIndex;
control.m_UsageCount = count;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithAliases(int startIndex, int count)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (startIndex < 0 || startIndex >= control.device.m_AliasesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0 || startIndex + count > control.device.m_AliasesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(count));
#endif
control.m_AliasStartIndex = startIndex;
control.m_AliasCount = count;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithChildren(int startIndex, int count)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (startIndex < 0 || startIndex >= control.device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0 || startIndex + count > control.device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(count));
#endif
control.m_ChildStartIndex = startIndex;
control.m_ChildCount = count;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithStateBlock(InputStateBlock stateBlock)
{
control.m_StateBlock = stateBlock;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithDefaultState(PrimitiveValue value)
{
control.m_DefaultState = value;
control.m_Device.hasControlsWithDefaultState = true;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithMinAndMax(PrimitiveValue min, PrimitiveValue max)
{
control.m_MinValue = min;
control.m_MaxValue = max;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder WithProcessor(TProcessor processor)
where TValue : struct
where TProcessor : InputProcessor
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (processor == null)
throw new ArgumentNullException(nameof(processor));
#endif
////REVIEW: have a parameterized version of ControlBuilder so we don't need the cast?
////TODO: size array to exact needed size before-hand
((InputControl)control).m_ProcessorStack.Append(processor);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder IsNoisy(bool value)
{
control.noisy = value;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder IsSynthetic(bool value)
{
control.synthetic = value;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder DontReset(bool value)
{
control.dontReset = value;
if (value)
control.m_Device.hasDontResetControls = true;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ControlBuilder IsButton(bool value)
{
control.isButton = value;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Finish()
{
control.isSetupFinished = true;
}
}
public struct DeviceBuilder
{
public InputDevice device { get; internal set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithName(string name)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
#endif
device.m_Name = new InternedString(name);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithDisplayName(string displayName)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(displayName))
throw new ArgumentNullException(nameof(displayName));
#endif
device.m_DisplayNameFromLayout = new InternedString(displayName);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithShortDisplayName(string shortDisplayName)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (string.IsNullOrEmpty(shortDisplayName))
throw new ArgumentNullException(nameof(shortDisplayName));
#endif
device.m_ShortDisplayNameFromLayout = new InternedString(shortDisplayName);
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithLayout(InternedString layout)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (layout.IsEmpty())
throw new ArgumentException("Layout name cannot be empty", nameof(layout));
#endif
device.m_Layout = layout;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithChildren(int startIndex, int count)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (startIndex < 0 || startIndex >= device.device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (count < 0 || startIndex + count > device.device.m_ChildrenForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(count));
#endif
device.m_ChildStartIndex = startIndex;
device.m_ChildCount = count;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithStateBlock(InputStateBlock stateBlock)
{
device.m_StateBlock = stateBlock;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder IsNoisy(bool value)
{
device.noisy = value;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithControlUsage(int controlIndex, InternedString usage, InputControl control)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (controlIndex < 0 || controlIndex >= device.m_UsagesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(controlIndex));
if (usage.IsEmpty())
throw new ArgumentException(nameof(usage));
if (control == null)
throw new ArgumentNullException(nameof(control));
#endif
device.m_UsagesForEachControl[controlIndex] = usage;
device.m_UsageToControl[controlIndex] = control;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithControlAlias(int controlIndex, InternedString alias)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (controlIndex < 0 || controlIndex >= device.m_AliasesForEachControl.Length)
throw new ArgumentOutOfRangeException(nameof(controlIndex));
if (alias.IsEmpty())
throw new ArgumentException(nameof(alias));
#endif
device.m_AliasesForEachControl[controlIndex] = alias;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DeviceBuilder WithStateOffsetToControlIndexMap(uint[] map)
{
device.m_StateOffsetToControlMap = map;
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Finish()
{
device.isSetupFinished = true;
}
}
#endregion
}
}