using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine.InputSystem.Utilities; ////TODO: add a device setup version to InputManager and add version check here to ensure we're not going out of sync ////REVIEW: can we have a read-only version of this ////REVIEW: move this to .LowLevel? this one is pretty peculiar to use and doesn't really work like what you'd expect given C#'s List<> namespace UnityEngine.InputSystem { /// /// Keep a list of s without allocating managed memory. /// /// /// This struct is mainly used by methods such as /// or to store an arbitrary length /// list of resulting matches without having to allocate GC heap memory. /// /// Requires the control setup in the system to not change while the list is being used. If devices are /// removed from the system, the list will no longer be valid. Also, only works with controls of devices that /// have been added to the system (). The reason for these constraints is /// that internally, the list only stores integer indices that are translates to /// references on the fly. If the device setup in the system changes, the indices may become invalid. /// /// This struct allocates unmanaged memory and thus must be disposed or it will leak memory. By default /// allocates Allocator.Persistent memory. You can direct it to use another allocator by /// passing an value to one of the constructors. /// /// /// /// // Find all controls with the "Submit" usage in the system. /// // By wrapping it in a `using` block, the list of controls will automatically be disposed at the end. /// using (var controls = InputSystem.FindControls("*/{Submit}")) /// /* ... */; /// /// /// /// Type of to store in the list. [DebuggerDisplay("Count = {Count}")] #if UNITY_EDITOR || DEVELOPMENT_BUILD [DebuggerTypeProxy(typeof(InputControlListDebugView<>))] #endif public unsafe struct InputControlList : IList, IReadOnlyList, IDisposable where TControl : InputControl { /// /// Current number of controls in the list. /// /// Number of controls currently in the list. public int Count => m_Count; /// /// Total number of controls that can currently be stored in the list. /// /// Total size of array as currently allocated. /// /// This can be set ahead of time to avoid repeated allocations. /// /// /// /// // Add all keys from the keyboard to a list. /// var keys = Keyboard.current.allKeys; /// var list = new InputControlList<KeyControl>(keys.Count); /// list.AddRange(keys); /// /// /// public int Capacity { get { if (!m_Indices.IsCreated) return 0; return m_Indices.Length; } set { if (value < 0) throw new ArgumentException("Capacity cannot be negative", nameof(value)); if (value == 0) { if (m_Count != 0) m_Indices.Dispose(); m_Count = 0; return; } var newSize = value; var allocator = m_Allocator != Allocator.Invalid ? m_Allocator : Allocator.Persistent; ArrayHelpers.Resize(ref m_Indices, newSize, allocator); } } /// /// This is always false. /// /// Always false. public bool IsReadOnly => false; /// /// Return the control at the given index. /// /// Index of control. /// is less than 0 or greater than or equal to /// /// /// Internally, the list only stores indices. Resolution to controls happens /// dynamically by looking them up globally. /// public TControl this[int index] { get { if (index < 0 || index >= m_Count) throw new ArgumentOutOfRangeException( nameof(index), $"Index {index} is out of range in list with {m_Count} entries"); return FromIndex(m_Indices[index]); } set { if (index < 0 || index >= m_Count) throw new ArgumentOutOfRangeException( nameof(index), $"Index {index} is out of range in list with {m_Count} entries"); m_Indices[index] = ToIndex(value); } } /// /// Construct a list that allocates unmanaged memory from the given allocator. /// /// Allocator to use for requesting unmanaged memory. /// If greater than zero, will immediately allocate /// memory and set accordingly. /// /// /// // Create a control list that allocates from the temporary memory allocator. /// using (var list = new InputControlList(Allocator.Temp)) /// { /// // Add all gamepads to the list. /// InputSystem.FindControls("<Gamepad>", ref list); /// } /// /// public InputControlList(Allocator allocator, int initialCapacity = 0) { m_Allocator = allocator; m_Indices = new NativeArray(); m_Count = 0; if (initialCapacity != 0) Capacity = initialCapacity; } /// /// Construct a list and populate it with the given values. /// /// Sequence of values to populate the list with. /// Allocator to use for requesting unmanaged memory. /// is null. public InputControlList(IEnumerable values, Allocator allocator = Allocator.Persistent) : this(allocator) { if (values == null) throw new ArgumentNullException(nameof(values)); foreach (var value in values) Add(value); } /// /// Construct a list and add the given values to it. /// /// Sequence of controls to add to the list. /// is null. public InputControlList(params TControl[] values) : this() { if (values == null) throw new ArgumentNullException(nameof(values)); var count = values.Length; Capacity = Mathf.Max(count, 10); for (var i = 0; i < count; ++i) Add(values[i]); } /// /// Resizes the list to be exactly entries. If this is less than the /// current , additional entries are dropped. If it is more than the /// current , additional null entries are appended to the list. /// /// The new value for . /// is negative. /// /// is increased if necessary. It will, however, not be decreased if it /// is larger than entries. /// public void Resize(int size) { if (size < 0) throw new ArgumentOutOfRangeException(nameof(size), "Size cannot be negative"); if (Capacity < size) Capacity = size; // Initialize newly added entries (if any) such that they produce NULL entries. if (size > Count) UnsafeUtility.MemSet((byte*)m_Indices.GetUnsafePtr() + Count * sizeof(ulong), Byte.MaxValue, size - Count); m_Count = size; } /// /// Add a control to the list. /// /// Control to add. Allowed to be null. /// /// If necessary, will be increased. /// /// It is allowed to add nulls to the list. This can be useful, for example, when /// specific indices in the list correlate with specific matches and a given match /// needs to be marked as "matches nothing". /// /// public void Add(TControl item) { var index = ToIndex(item); var allocator = m_Allocator != Allocator.Invalid ? m_Allocator : Allocator.Persistent; ArrayHelpers.AppendWithCapacity(ref m_Indices, ref m_Count, index, allocator: allocator); } /// /// Add a slice of elements taken from the given list. /// /// List to take the slice of values from. /// Number of elements to copy from . /// Starting index in the current control list to copy to. /// This can be beyond or even . Memory is allocated /// as needed. /// Source index in to start copying from. /// elements are copied starting at . /// Type of list. This is a type parameter to avoid boxing in case the /// given list is a struct (such as InputControlList itself). /// The range of /// and is at least partially outside the range of values /// available in . public void AddSlice(TList list, int count = -1, int destinationIndex = -1, int sourceIndex = 0) where TList : IReadOnlyList { if (count < 0) count = list.Count; if (destinationIndex < 0) destinationIndex = Count; if (count == 0) return; if (sourceIndex + count > list.Count) throw new ArgumentOutOfRangeException(nameof(count), $"Count of {count} elements starting at index {sourceIndex} exceeds length of list of {list.Count}"); // Make space in the list. if (Capacity < m_Count + count) Capacity = Math.Max(m_Count + count, 10); if (destinationIndex < Count) NativeArray.Copy(m_Indices, destinationIndex, m_Indices, destinationIndex + count, Count - destinationIndex); // Add elements. for (var i = 0; i < count; ++i) m_Indices[destinationIndex + i] = ToIndex(list[sourceIndex + i]); m_Count += count; } /// /// Add a sequence of controls to the list. /// /// Sequence of controls to add. /// Number of controls from to add. If negative /// (default), all controls from will be added. /// Index in the control list to start inserting controls /// at. If negative (default), controls will be appended to the end of the control list. /// is null. /// /// If is not supplied, will be iterated /// over twice. /// public void AddRange(IEnumerable list, int count = -1, int destinationIndex = -1) { if (list == null) throw new ArgumentNullException(nameof(list)); if (count < 0) count = list.Count(); if (destinationIndex < 0) destinationIndex = Count; if (count == 0) return; // Make space in the list. if (Capacity < m_Count + count) Capacity = Math.Max(m_Count + count, 10); if (destinationIndex < Count) NativeArray.Copy(m_Indices, destinationIndex, m_Indices, destinationIndex + count, Count - destinationIndex); // Add elements. foreach (var element in list) { m_Indices[destinationIndex++] = ToIndex(element); ++m_Count; --count; if (count == 0) break; } } /// /// Remove a control from the list. /// /// Control to remove. Can be null. /// True if the control was found in the list and removed, false otherwise. /// public bool Remove(TControl item) { if (m_Count == 0) return false; var index = ToIndex(item); for (var i = 0; i < m_Count; ++i) { if (m_Indices[i] == index) { ArrayHelpers.EraseAtWithCapacity(m_Indices, ref m_Count, i); return true; } } return false; } /// /// Remove the control at the given index. /// /// Index of control to remove. /// is negative or equal /// or greater than . public void RemoveAt(int index) { if (index < 0 || index >= m_Count) throw new ArgumentOutOfRangeException( nameof(index), $"Index {index} is out of range in list with {m_Count} elements"); ArrayHelpers.EraseAtWithCapacity(m_Indices, ref m_Count, index); } public void CopyTo(TControl[] array, int arrayIndex) { throw new NotImplementedException(); } public int IndexOf(TControl item) { return IndexOf(item, 0); } public int IndexOf(TControl item, int startIndex, int count = -1) { if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), "startIndex cannot be negative"); if (m_Count == 0) return -1; if (count < 0) count = Mathf.Max(m_Count - startIndex, 0); if (startIndex + count > m_Count) throw new ArgumentOutOfRangeException(nameof(count)); var index = ToIndex(item); var indices = (ulong*)m_Indices.GetUnsafeReadOnlyPtr(); for (var i = 0; i < count; ++i) if (indices[startIndex + i] == index) return startIndex + i; return -1; } public void Insert(int index, TControl item) { throw new NotImplementedException(); } public void Clear() { m_Count = 0; } public bool Contains(TControl item) { return IndexOf(item) != -1; } public bool Contains(TControl item, int startIndex, int count = -1) { return IndexOf(item, startIndex, count) != -1; } public void SwapElements(int index1, int index2) { if (index1 < 0 || index1 >= m_Count) throw new ArgumentOutOfRangeException(nameof(index1)); if (index2 < 0 || index2 >= m_Count) throw new ArgumentOutOfRangeException(nameof(index2)); if (index1 != index2) m_Indices.SwapElements(index1, index2); } public void Sort(int startIndex, int count, TCompare comparer) where TCompare : IComparer { if (startIndex < 0 || startIndex >= Count) throw new ArgumentOutOfRangeException(nameof(startIndex)); if (startIndex + count >= Count) throw new ArgumentOutOfRangeException(nameof(count)); // Simple insertion sort. for (var i = 1; i < count; ++i) for (var j = i; j > 0 && comparer.Compare(this[j - 1], this[j]) < 0; --j) SwapElements(j, j - 1); } /// /// Convert the contents of the list to an array. /// /// If true, the control list will be disposed of as part of the operation, i.e. /// will be called as a side-effect. /// An array mirroring the contents of the list. Not null. public TControl[] ToArray(bool dispose = false) { // Somewhat pointless to allocate an empty array if we have no elements instead // of returning null, but other ToArray() implementations work that way so we do // the same to avoid surprises. var result = new TControl[m_Count]; for (var i = 0; i < m_Count; ++i) result[i] = this[i]; if (dispose) Dispose(); return result; } internal void AppendTo(ref TControl[] array, ref int count) { for (var i = 0; i < m_Count; ++i) ArrayHelpers.AppendWithCapacity(ref array, ref count, this[i]); } public void Dispose() { if (m_Indices.IsCreated) m_Indices.Dispose(); } public IEnumerator GetEnumerator() { return new Enumerator(this); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public override string ToString() { if (Count == 0) return "()"; var builder = new StringBuilder(); builder.Append('('); for (var i = 0; i < Count; ++i) { if (i != 0) builder.Append(','); builder.Append(this[i]); } builder.Append(')'); return builder.ToString(); } private int m_Count; private NativeArray m_Indices; private readonly Allocator m_Allocator; private const ulong kInvalidIndex = 0xffffffffffffffff; private static ulong ToIndex(TControl control) { if (control == null) return kInvalidIndex; var device = control.device; var deviceId = device.m_DeviceId; var controlIndex = !ReferenceEquals(device, control) ? device.m_ChildrenForEachControl.IndexOfReference(control) + 1 : 0; // There is a known documented bug with the new Rosyln // compiler where it warns on casts with following line that // was perfectly legal in previous CSC compiler. // Below is silly conversion to get rid of warning, or we can pragma // out the warning. //return ((ulong)deviceId << 32) | (ulong)controlIndex; var shiftedDeviceId = (ulong)deviceId << 32; var unsignedControlIndex = (ulong)controlIndex; return shiftedDeviceId | unsignedControlIndex; } private static TControl FromIndex(ulong index) { if (index == kInvalidIndex) return null; var deviceId = (int)(index >> 32); var controlIndex = (int)(index & 0xFFFFFFFF); var device = InputSystem.GetDeviceById(deviceId); if (device == null) return null; if (controlIndex == 0) return (TControl)(InputControl)device; return (TControl)device.m_ChildrenForEachControl[controlIndex - 1]; } private struct Enumerator : IEnumerator { private readonly ulong* m_Indices; private readonly int m_Count; private int m_Current; public Enumerator(InputControlList list) { m_Count = list.m_Count; m_Current = -1; m_Indices = m_Count > 0 ? (ulong*)list.m_Indices.GetUnsafeReadOnlyPtr() : null; } public bool MoveNext() { if (m_Current >= m_Count) return false; ++m_Current; return (m_Current != m_Count); } public void Reset() { m_Current = -1; } public TControl Current { get { if (m_Indices == null) throw new InvalidOperationException("Enumerator is not valid"); return FromIndex(m_Indices[m_Current]); } } object IEnumerator.Current => Current; public void Dispose() { } } } #if UNITY_EDITOR || DEVELOPMENT_BUILD internal struct InputControlListDebugView where TControl : InputControl { private readonly TControl[] m_Controls; public InputControlListDebugView(InputControlList list) { m_Controls = list.ToArray(); } public TControl[] controls => m_Controls; } #endif }