using System; using System.Collections.Generic; 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; } /// /// 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) { ////REVIEW: we probably want to do a value comparison on this path to compare it to the default value return true; } 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; } 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; } 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); } 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); } 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)); return control.CompareValue(control.currentStatePtr, control.GetStatePtrFromStateEvent(eventPtr)); } /// /// 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)); uint stateOffset; FourCC stateFormat; uint stateSizeInBytes; void* statePtr; if (eventPtr.IsA()) { var deltaEvent = DeltaStateEvent.From(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 if (eventPtr.IsA()) { var stateEvent = StateEvent.From(eventPtr); stateOffset = 0; stateFormat = stateEvent->stateFormat; stateSizeInBytes = stateEvent->stateSizeInBytes; statePtr = stateEvent->state; } else { throw new ArgumentException("Event must be a state or delta state event", 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. var controlOffset = (int)control.m_StateBlock.byteOffset - stateOffset; if (controlOffset < 0 || controlOffset + control.m_StateBlock.alignedSizeInBytes > stateSizeInBytes) return null; return (byte*)statePtr - (int)stateOffset; } /// /// 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, the event will not /// be processed until it falls within the time of an input update slice (except if /// is false, in which case the event will invariably be consumed in the next update). /// 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 newDelta)) return; // Value for the control not contained in the given event. var oldDelta = control.ReadUnprocessedValueFromState(currentStatePtr); control.WriteValueIntoEvent(oldDelta + newDelta, 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); 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]); builder.Append('}'); } builder.Append('/'); var devicePath = device.path; var controlPath = control.path; builder.Append(controlPath, devicePath.Length + 1, controlPath.Length - devicePath.Length - 1); return builder.ToString(); } } }