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
}