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.magnitude; 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 . /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1714:Flags enums should have plural names", Justification = "False positive: `IgnoreControlsInDefaultState` is a plural form.")] [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 unsafe DeviceBuilder WithControlTree(byte[] controlTreeNodes, ushort[] controlTreeIndicies) { var sizeOfNode = UnsafeUtility.SizeOf(); var numNodes = controlTreeNodes.Length / sizeOfNode; device.m_ControlTreeNodes = new InputDevice.ControlBitRangeNode[numNodes]; fixed(byte* nodePtr = controlTreeNodes) { for (var i = 0; i < numNodes; i++) { device.m_ControlTreeNodes[i] = *(InputDevice.ControlBitRangeNode*)(nodePtr + i * sizeOfNode); } } device.m_ControlTreeIndices = controlTreeIndicies; return this; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Finish() { // Set up the list of just ButtonControls to quickly update press state. var i = 0; foreach (var control in device.allControls) { // Don't use .isButton here, since this can be called from tests with NULL controls if (control is ButtonControl) ++i; } device.m_ButtonControlsCheckingPressState = new List(i); #if UNITY_2020_1_OR_NEWER device.m_UpdatedButtons = new HashSet(i); #else // 2019 is too old to support setting HashSet capacity device.m_UpdatedButtons = new HashSet(); #endif device.isSetupFinished = true; } } #endregion } }