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&lt;KeyControl&gt;(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("&lt;Gamepad&gt;", 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}