A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Diagnostics;
5using System.Linq;
6using System.Text;
7using Unity.Collections;
8using Unity.Collections.LowLevel.Unsafe;
9using UnityEngine.InputSystem.Utilities;
10
11////TODO: add a device setup version to InputManager and add version check here to ensure we're not going out of sync
12
13////REVIEW: can we have a read-only version of this
14
15////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<>
16
17namespace UnityEngine.InputSystem
18{
19 /// <summary>
20 /// Keep a list of <see cref="InputControl"/>s without allocating managed memory.
21 /// </summary>
22 /// <remarks>
23 /// This struct is mainly used by methods such as <see cref="InputSystem.FindControls(string)"/>
24 /// or <see cref="InputControlPath.TryFindControls{TControl}"/> to store an arbitrary length
25 /// list of resulting matches without having to allocate GC heap memory.
26 ///
27 /// Requires the control setup in the system to not change while the list is being used. If devices are
28 /// removed from the system, the list will no longer be valid. Also, only works with controls of devices that
29 /// have been added to the system (<see cref="InputDevice.added"/>). The reason for these constraints is
30 /// that internally, the list only stores integer indices that are translates to <see cref="InputControl"/>
31 /// references on the fly. If the device setup in the system changes, the indices may become invalid.
32 ///
33 /// This struct allocates unmanaged memory and thus must be disposed or it will leak memory. By default
34 /// allocates <c>Allocator.Persistent</c> memory. You can direct it to use another allocator by
35 /// passing an <see cref="Allocator"/> value to one of the constructors.
36 ///
37 /// <example>
38 /// <code>
39 /// // Find all controls with the "Submit" usage in the system.
40 /// // By wrapping it in a `using` block, the list of controls will automatically be disposed at the end.
41 /// using (var controls = InputSystem.FindControls("*/{Submit}"))
42 /// /* ... */;
43 /// </code>
44 /// </example>
45 /// </remarks>
46 /// <typeparam name="TControl">Type of <see cref="InputControl"/> to store in the list.</typeparam>
47 [DebuggerDisplay("Count = {Count}")]
48 #if UNITY_EDITOR || DEVELOPMENT_BUILD
49 [DebuggerTypeProxy(typeof(InputControlListDebugView<>))]
50 #endif
51 public unsafe struct InputControlList<TControl> : IList<TControl>, IReadOnlyList<TControl>, IDisposable
52 where TControl : InputControl
53 {
54 /// <summary>
55 /// Current number of controls in the list.
56 /// </summary>
57 /// <value>Number of controls currently in the list.</value>
58 public int Count => m_Count;
59
60 /// <summary>
61 /// Total number of controls that can currently be stored in the list.
62 /// </summary>
63 /// <value>Total size of array as currently allocated.</value>
64 /// <remarks>
65 /// This can be set ahead of time to avoid repeated allocations.
66 ///
67 /// <example>
68 /// <code>
69 /// // Add all keys from the keyboard to a list.
70 /// var keys = Keyboard.current.allKeys;
71 /// var list = new InputControlList<KeyControl>(keys.Count);
72 /// list.AddRange(keys);
73 /// </code>
74 /// </example>
75 /// </remarks>
76 public int Capacity
77 {
78 get
79 {
80 if (!m_Indices.IsCreated)
81 return 0;
82 return m_Indices.Length;
83 }
84 set
85 {
86 if (value < 0)
87 throw new ArgumentException("Capacity cannot be negative", nameof(value));
88
89 if (value == 0)
90 {
91 if (m_Count != 0)
92 m_Indices.Dispose();
93 m_Count = 0;
94 return;
95 }
96
97 var newSize = value;
98 var allocator = m_Allocator != Allocator.Invalid ? m_Allocator : Allocator.Persistent;
99 ArrayHelpers.Resize(ref m_Indices, newSize, allocator);
100 }
101 }
102
103 /// <summary>
104 /// This is always false.
105 /// </summary>
106 /// <value>Always false.</value>
107 public bool IsReadOnly => false;
108
109 /// <summary>
110 /// Return the control at the given index.
111 /// </summary>
112 /// <param name="index">Index of control.</param>
113 /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or greater than or equal to <see cref="Count"/>
114 /// </exception>
115 /// <remarks>
116 /// Internally, the list only stores indices. Resolution to <see cref="InputControl">controls</see> happens
117 /// dynamically by looking them up globally.
118 /// </remarks>
119 public TControl this[int index]
120 {
121 get
122 {
123 if (index < 0 || index >= m_Count)
124 throw new ArgumentOutOfRangeException(
125 nameof(index), $"Index {index} is out of range in list with {m_Count} entries");
126
127 return FromIndex(m_Indices[index]);
128 }
129 set
130 {
131 if (index < 0 || index >= m_Count)
132 throw new ArgumentOutOfRangeException(
133 nameof(index), $"Index {index} is out of range in list with {m_Count} entries");
134
135 m_Indices[index] = ToIndex(value);
136 }
137 }
138
139 /// <summary>
140 /// Construct a list that allocates unmanaged memory from the given allocator.
141 /// </summary>
142 /// <param name="allocator">Allocator to use for requesting unmanaged memory.</param>
143 /// <param name="initialCapacity">If greater than zero, will immediately allocate
144 /// memory and set <see cref="Capacity"/> accordingly.</param>
145 /// <example>
146 /// <code>
147 /// // Create a control list that allocates from the temporary memory allocator.
148 /// using (var list = new InputControlList(Allocator.Temp))
149 /// {
150 /// // Add all gamepads to the list.
151 /// InputSystem.FindControls("<Gamepad>", ref list);
152 /// }
153 /// </code>
154 /// </example>
155 public InputControlList(Allocator allocator, int initialCapacity = 0)
156 {
157 m_Allocator = allocator;
158 m_Indices = new NativeArray<ulong>();
159 m_Count = 0;
160
161 if (initialCapacity != 0)
162 Capacity = initialCapacity;
163 }
164
165 /// <summary>
166 /// Construct a list and populate it with the given values.
167 /// </summary>
168 /// <param name="values">Sequence of values to populate the list with.</param>
169 /// <param name="allocator">Allocator to use for requesting unmanaged memory.</param>
170 /// <exception cref="ArgumentNullException"><paramref name="values"/> is <c>null</c>.</exception>
171 public InputControlList(IEnumerable<TControl> values, Allocator allocator = Allocator.Persistent)
172 : this(allocator)
173 {
174 if (values == null)
175 throw new ArgumentNullException(nameof(values));
176
177 foreach (var value in values)
178 Add(value);
179 }
180
181 /// <summary>
182 /// Construct a list and add the given values to it.
183 /// </summary>
184 /// <param name="values">Sequence of controls to add to the list.</param>
185 /// <exception cref="ArgumentNullException"><paramref name="values"/> is null.</exception>
186 public InputControlList(params TControl[] values)
187 : this()
188 {
189 if (values == null)
190 throw new ArgumentNullException(nameof(values));
191
192 var count = values.Length;
193 Capacity = Mathf.Max(count, 10);
194 for (var i = 0; i < count; ++i)
195 Add(values[i]);
196 }
197
198 /// <summary>
199 /// Resizes the list to be exactly <paramref name="size"/> entries. If this is less than the
200 /// current <see cref="Count"/>, additional entries are dropped. If it is more than the
201 /// current <see cref="Count"/>, additional <c>null</c> entries are appended to the list.
202 /// </summary>
203 /// <param name="size">The new value for <see cref="Count"/>.</param>
204 /// <exception cref="ArgumentOutOfRangeException"><paramref name="size"/> is negative.</exception>
205 /// <remarks>
206 /// <see cref="Capacity"/> is increased if necessary. It will, however, not be decreased if it
207 /// is larger than <paramref name="size"/> entries.
208 /// </remarks>
209 public void Resize(int size)
210 {
211 if (size < 0)
212 throw new ArgumentOutOfRangeException(nameof(size), "Size cannot be negative");
213
214 if (Capacity < size)
215 Capacity = size;
216
217 // Initialize newly added entries (if any) such that they produce NULL entries.
218 if (size > Count)
219 UnsafeUtility.MemSet((byte*)m_Indices.GetUnsafePtr() + Count * sizeof(ulong), Byte.MaxValue, size - Count);
220
221 m_Count = size;
222 }
223
224 /// <summary>
225 /// Add a control to the list.
226 /// </summary>
227 /// <param name="item">Control to add. Allowed to be <c>null</c>.</param>
228 /// <remarks>
229 /// If necessary, <see cref="Capacity"/> will be increased.
230 ///
231 /// It is allowed to add nulls to the list. This can be useful, for example, when
232 /// specific indices in the list correlate with specific matches and a given match
233 /// needs to be marked as "matches nothing".
234 /// </remarks>
235 /// <seealso cref="Remove"/>
236 public void Add(TControl item)
237 {
238 var index = ToIndex(item);
239 var allocator = m_Allocator != Allocator.Invalid ? m_Allocator : Allocator.Persistent;
240 ArrayHelpers.AppendWithCapacity(ref m_Indices, ref m_Count, index, allocator: allocator);
241 }
242
243 /// <summary>
244 /// Add a slice of elements taken from the given list.
245 /// </summary>
246 /// <param name="list">List to take the slice of values from.</param>
247 /// <param name="count">Number of elements to copy from <paramref name="list"/>.</param>
248 /// <param name="destinationIndex">Starting index in the current control list to copy to.
249 /// This can be beyond <see cref="Count"/> or even <see cref="Capacity"/>. Memory is allocated
250 /// as needed.</param>
251 /// <param name="sourceIndex">Source index in <paramref name="list"/> to start copying from.
252 /// <paramref name="count"/> elements are copied starting at <paramref name="sourceIndex"/>.</param>
253 /// <typeparam name="TList">Type of list. This is a type parameter to avoid boxing in case the
254 /// given list is a struct (such as InputControlList itself).</typeparam>
255 /// <exception cref="ArgumentOutOfRangeException">The range of <paramref name="count"/>
256 /// and <paramref name="sourceIndex"/> is at least partially outside the range of values
257 /// available in <paramref name="list"/>.</exception>
258 public void AddSlice<TList>(TList list, int count = -1, int destinationIndex = -1, int sourceIndex = 0)
259 where TList : IReadOnlyList<TControl>
260 {
261 if (count < 0)
262 count = list.Count;
263 if (destinationIndex < 0)
264 destinationIndex = Count;
265
266 if (count == 0)
267 return;
268 if (sourceIndex + count > list.Count)
269 throw new ArgumentOutOfRangeException(nameof(count),
270 $"Count of {count} elements starting at index {sourceIndex} exceeds length of list of {list.Count}");
271
272 // Make space in the list.
273 if (Capacity < m_Count + count)
274 Capacity = Math.Max(m_Count + count, 10);
275 if (destinationIndex < Count)
276 NativeArray<ulong>.Copy(m_Indices, destinationIndex, m_Indices, destinationIndex + count,
277 Count - destinationIndex);
278
279 // Add elements.
280 for (var i = 0; i < count; ++i)
281 m_Indices[destinationIndex + i] = ToIndex(list[sourceIndex + i]);
282 m_Count += count;
283 }
284
285 /// <summary>
286 /// Add a sequence of controls to the list.
287 /// </summary>
288 /// <param name="list">Sequence of controls to add.</param>
289 /// <param name="count">Number of controls from <paramref name="list"/> to add. If negative
290 /// (default), all controls from <paramref name="list"/> will be added.</param>
291 /// <param name="destinationIndex">Index in the control list to start inserting controls
292 /// at. If negative (default), controls will be appended to the end of the control list.</param>
293 /// <exception cref="ArgumentNullException"><paramref name="list"/> is <c>null</c>.</exception>
294 /// <remarks>
295 /// If <paramref name="count"/> is not supplied, <paramref name="list"/> will be iterated
296 /// over twice.
297 /// </remarks>
298 public void AddRange(IEnumerable<TControl> list, int count = -1, int destinationIndex = -1)
299 {
300 if (list == null)
301 throw new ArgumentNullException(nameof(list));
302
303 if (count < 0)
304 count = list.Count();
305 if (destinationIndex < 0)
306 destinationIndex = Count;
307
308 if (count == 0)
309 return;
310
311 // Make space in the list.
312 if (Capacity < m_Count + count)
313 Capacity = Math.Max(m_Count + count, 10);
314 if (destinationIndex < Count)
315 NativeArray<ulong>.Copy(m_Indices, destinationIndex, m_Indices, destinationIndex + count,
316 Count - destinationIndex);
317
318 // Add elements.
319 foreach (var element in list)
320 {
321 m_Indices[destinationIndex++] = ToIndex(element);
322 ++m_Count;
323 --count;
324 if (count == 0)
325 break;
326 }
327 }
328
329 /// <summary>
330 /// Remove a control from the list.
331 /// </summary>
332 /// <param name="item">Control to remove. Can be null.</param>
333 /// <returns>True if the control was found in the list and removed, false otherwise.</returns>
334 /// <seealso cref="Add"/>
335 public bool Remove(TControl item)
336 {
337 if (m_Count == 0)
338 return false;
339
340 var index = ToIndex(item);
341 for (var i = 0; i < m_Count; ++i)
342 {
343 if (m_Indices[i] == index)
344 {
345 ArrayHelpers.EraseAtWithCapacity(m_Indices, ref m_Count, i);
346 return true;
347 }
348 }
349
350 return false;
351 }
352
353 /// <summary>
354 /// Remove the control at the given index.
355 /// </summary>
356 /// <param name="index">Index of control to remove.</param>
357 /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is negative or equal
358 /// or greater than <see cref="Count"/>.</exception>
359 public void RemoveAt(int index)
360 {
361 if (index < 0 || index >= m_Count)
362 throw new ArgumentOutOfRangeException(
363 nameof(index), $"Index {index} is out of range in list with {m_Count} elements");
364
365 ArrayHelpers.EraseAtWithCapacity(m_Indices, ref m_Count, index);
366 }
367
368 public void CopyTo(TControl[] array, int arrayIndex)
369 {
370 throw new NotImplementedException();
371 }
372
373 public int IndexOf(TControl item)
374 {
375 return IndexOf(item, 0);
376 }
377
378 public int IndexOf(TControl item, int startIndex, int count = -1)
379 {
380 if (startIndex < 0)
381 throw new ArgumentOutOfRangeException(nameof(startIndex), "startIndex cannot be negative");
382
383 if (m_Count == 0)
384 return -1;
385
386 if (count < 0)
387 count = Mathf.Max(m_Count - startIndex, 0);
388
389 if (startIndex + count > m_Count)
390 throw new ArgumentOutOfRangeException(nameof(count));
391
392 var index = ToIndex(item);
393 var indices = (ulong*)m_Indices.GetUnsafeReadOnlyPtr();
394
395 for (var i = 0; i < count; ++i)
396 if (indices[startIndex + i] == index)
397 return startIndex + i;
398
399 return -1;
400 }
401
402 public void Insert(int index, TControl item)
403 {
404 throw new NotImplementedException();
405 }
406
407 public void Clear()
408 {
409 m_Count = 0;
410 }
411
412 public bool Contains(TControl item)
413 {
414 return IndexOf(item) != -1;
415 }
416
417 public bool Contains(TControl item, int startIndex, int count = -1)
418 {
419 return IndexOf(item, startIndex, count) != -1;
420 }
421
422 public void SwapElements(int index1, int index2)
423 {
424 if (index1 < 0 || index1 >= m_Count)
425 throw new ArgumentOutOfRangeException(nameof(index1));
426 if (index2 < 0 || index2 >= m_Count)
427 throw new ArgumentOutOfRangeException(nameof(index2));
428
429 if (index1 != index2)
430 m_Indices.SwapElements(index1, index2);
431 }
432
433 public void Sort<TCompare>(int startIndex, int count, TCompare comparer)
434 where TCompare : IComparer<TControl>
435 {
436 if (startIndex < 0 || startIndex >= Count)
437 throw new ArgumentOutOfRangeException(nameof(startIndex));
438 if (startIndex + count >= Count)
439 throw new ArgumentOutOfRangeException(nameof(count));
440
441 // Simple insertion sort.
442 for (var i = 1; i < count; ++i)
443 for (var j = i; j > 0 && comparer.Compare(this[j - 1], this[j]) < 0; --j)
444 SwapElements(j, j - 1);
445 }
446
447 /// <summary>
448 /// Convert the contents of the list to an array.
449 /// </summary>
450 /// <param name="dispose">If true, the control list will be disposed of as part of the operation, i.e.
451 /// <see cref="Dispose"/> will be called as a side-effect.</param>
452 /// <returns>An array mirroring the contents of the list. Not null.</returns>
453 public TControl[] ToArray(bool dispose = false)
454 {
455 // Somewhat pointless to allocate an empty array if we have no elements instead
456 // of returning null, but other ToArray() implementations work that way so we do
457 // the same to avoid surprises.
458
459 var result = new TControl[m_Count];
460 for (var i = 0; i < m_Count; ++i)
461 result[i] = this[i];
462
463 if (dispose)
464 Dispose();
465
466 return result;
467 }
468
469 internal void AppendTo(ref TControl[] array, ref int count)
470 {
471 for (var i = 0; i < m_Count; ++i)
472 ArrayHelpers.AppendWithCapacity(ref array, ref count, this[i]);
473 }
474
475 public void Dispose()
476 {
477 if (m_Indices.IsCreated)
478 m_Indices.Dispose();
479 }
480
481 public IEnumerator<TControl> GetEnumerator()
482 {
483 return new Enumerator(this);
484 }
485
486 IEnumerator IEnumerable.GetEnumerator()
487 {
488 return GetEnumerator();
489 }
490
491 public override string ToString()
492 {
493 if (Count == 0)
494 return "()";
495
496 var builder = new StringBuilder();
497 builder.Append('(');
498
499 for (var i = 0; i < Count; ++i)
500 {
501 if (i != 0)
502 builder.Append(',');
503 builder.Append(this[i]);
504 }
505
506 builder.Append(')');
507 return builder.ToString();
508 }
509
510 private int m_Count;
511 private NativeArray<ulong> m_Indices;
512 private readonly Allocator m_Allocator;
513
514 private const ulong kInvalidIndex = 0xffffffffffffffff;
515
516 private static ulong ToIndex(TControl control)
517 {
518 if (control == null)
519 return kInvalidIndex;
520
521 var device = control.device;
522 var deviceId = device.m_DeviceId;
523 var controlIndex = !ReferenceEquals(device, control)
524 ? device.m_ChildrenForEachControl.IndexOfReference<InputControl, InputControl>(control) + 1
525 : 0;
526
527 // There is a known documented bug with the new Rosyln
528 // compiler where it warns on casts with following line that
529 // was perfectly legal in previous CSC compiler.
530 // Below is silly conversion to get rid of warning, or we can pragma
531 // out the warning.
532 //return ((ulong)deviceId << 32) | (ulong)controlIndex;
533 var shiftedDeviceId = (ulong)deviceId << 32;
534 var unsignedControlIndex = (ulong)controlIndex;
535
536 return shiftedDeviceId | unsignedControlIndex;
537 }
538
539 private static TControl FromIndex(ulong index)
540 {
541 if (index == kInvalidIndex)
542 return null;
543
544 var deviceId = (int)(index >> 32);
545 var controlIndex = (int)(index & 0xFFFFFFFF);
546
547 var device = InputSystem.GetDeviceById(deviceId);
548 if (device == null)
549 return null;
550 if (controlIndex == 0)
551 return (TControl)(InputControl)device;
552
553 return (TControl)device.m_ChildrenForEachControl[controlIndex - 1];
554 }
555
556 private struct Enumerator : IEnumerator<TControl>
557 {
558 private readonly ulong* m_Indices;
559 private readonly int m_Count;
560 private int m_Current;
561
562 public Enumerator(InputControlList<TControl> list)
563 {
564 m_Count = list.m_Count;
565 m_Current = -1;
566 m_Indices = m_Count > 0 ? (ulong*)list.m_Indices.GetUnsafeReadOnlyPtr() : null;
567 }
568
569 public bool MoveNext()
570 {
571 if (m_Current >= m_Count)
572 return false;
573 ++m_Current;
574 return (m_Current != m_Count);
575 }
576
577 public void Reset()
578 {
579 m_Current = -1;
580 }
581
582 public TControl Current
583 {
584 get
585 {
586 if (m_Indices == null)
587 throw new InvalidOperationException("Enumerator is not valid");
588 return FromIndex(m_Indices[m_Current]);
589 }
590 }
591
592 object IEnumerator.Current => Current;
593
594 public void Dispose()
595 {
596 }
597 }
598 }
599
600 #if UNITY_EDITOR || DEVELOPMENT_BUILD
601 internal struct InputControlListDebugView<TControl>
602 where TControl : InputControl
603 {
604 private readonly TControl[] m_Controls;
605
606 public InputControlListDebugView(InputControlList<TControl> list)
607 {
608 m_Controls = list.ToArray();
609 }
610
611 public TControl[] controls => m_Controls;
612 }
613 #endif
614}