A game about forced loneliness, made by TACStudios
1using System; 2using System.Collections; 3using System.Collections.Generic; 4using System.Linq; 5using System.Runtime.InteropServices; 6using Unity.Collections; 7using Unity.Collections.LowLevel.Unsafe; 8using UnityEngine.InputSystem; 9using UnityEngine.InputSystem.LowLevel; 10using UnityEngine.InputSystem.Utilities; 11 12////REVIEW: should this enumerate *backwards* in time rather than *forwards*? 13 14////TODO: allow correlating history to frames/updates 15 16////TODO: add ability to grow such that you can set it to e.g. record up to 4 seconds of history and it will automatically keep the buffer size bounded 17 18////REVIEW: should we align the extra memory on a 4 byte boundary? 19 20namespace UnityEngine.InputSystem.LowLevel 21{ 22 /// <summary> 23 /// Record a history of state changes applied to one or more controls. 24 /// </summary> 25 /// <remarks> 26 /// This class makes it easy to track input values over time. It will automatically retain input state up to a given 27 /// maximum history depth (<see cref="historyDepth"/>). When the history is full, it will start overwriting the oldest 28 /// entry each time a new history record is received. 29 /// 30 /// The class listens to changes on the given controls by adding change monitors (<see cref="IInputStateChangeMonitor"/>) 31 /// to each control. 32 /// 33 /// <example> 34 /// <code> 35 /// // Track all stick controls in the system. 36 /// var history = new InputStateHistory&lt;Vector2&gt;("*/&lt;Stick&gt;"); 37 /// foreach (var control in history.controls) 38 /// Debug.Log("Capturing input on " + control); 39 /// 40 /// // Start capturing. 41 /// history.StartRecording(); 42 /// 43 /// // Perform a couple artificial value changes. 44 /// Gamepad.current.leftStick.QueueValueChange(new Vector2(0.123f, 0.234f)); 45 /// Gamepad.current.leftStick.QueueValueChange(new Vector2(0.234f, 0.345f)); 46 /// Gamepad.current.leftStick.QueueValueChange(new Vector2(0.345f, 0.456f)); 47 /// InputSystem.Update(); 48 /// 49 /// // Every value change will be visible in the history. 50 /// foreach (var record in history) 51 /// Debug.Log($"{record.control} changed value to {record.ReadValue()}"); 52 /// 53 /// // Histories allocate unmanaged memory and must be disposed of in order to not leak. 54 /// history.Dispose(); 55 /// </code> 56 /// </example> 57 /// </remarks> 58 public class InputStateHistory : IDisposable, IEnumerable<InputStateHistory.Record>, IInputStateChangeMonitor 59 { 60 private const int kDefaultHistorySize = 128; 61 62 /// <summary> 63 /// Total number of state records currently captured in the history. 64 /// </summary> 65 /// <value>Number of records in the collection.</value> 66 /// <remarks> 67 /// This will always be at most <see cref="historyDepth"/>. 68 /// </remarks> 69 /// <seealso cref="historyDepth"/> 70 /// <seealso cref="RecordStateChange(InputControl,InputEventPtr)"/> 71 public int Count => m_RecordCount; 72 73 /// <summary> 74 /// Current version stamp. Every time a record is stored in the history, 75 /// this is incremented by one. 76 /// </summary> 77 /// <value>Version stamp that indicates the number of mutations.</value> 78 /// <seealso cref="RecordStateChange(InputControl,InputEventPtr)"/> 79 public uint version => m_CurrentVersion; 80 81 /// <summary> 82 /// Maximum number of records that can be recorded in the history. 83 /// </summary> 84 /// <value>Upper limit on number of records.</value> 85 /// <exception cref="ArgumentException"><paramref name="value"/> is negative.</exception> 86 /// <remarks> 87 /// A fixed size memory block of unmanaged memory will be allocated to store history 88 /// records. This property determines TODO 89 /// </remarks> 90 public int historyDepth 91 { 92 get => m_HistoryDepth; 93 set 94 { 95 if (value < 0) 96 throw new ArgumentException("History depth cannot be negative", nameof(value)); 97 if (m_RecordBuffer.IsCreated) 98 throw new NotImplementedException(); 99 m_HistoryDepth = value; 100 } 101 } 102 103 public int extraMemoryPerRecord 104 { 105 get => m_ExtraMemoryPerRecord; 106 set 107 { 108 if (value < 0) 109 throw new ArgumentException("Memory size cannot be negative", nameof(value)); 110 if (m_RecordBuffer.IsCreated) 111 throw new NotImplementedException(); 112 m_ExtraMemoryPerRecord = value; 113 } 114 } 115 116 public InputUpdateType updateMask 117 { 118 get => m_UpdateMask ?? InputSystem.s_Manager.updateMask & ~InputUpdateType.Editor; 119 set 120 { 121 if (value == InputUpdateType.None) 122 throw new ArgumentException("'InputUpdateType.None' is not a valid update mask", nameof(value)); 123 m_UpdateMask = value; 124 } 125 } 126 127 public ReadOnlyArray<InputControl> controls => new ReadOnlyArray<InputControl>(m_Controls, 0, m_ControlCount); 128 129 public unsafe Record this[int index] 130 { 131 get 132 { 133 if (index < 0 || index >= m_RecordCount) 134 throw new ArgumentOutOfRangeException( 135 $"Index {index} is out of range for history with {m_RecordCount} entries", nameof(index)); 136 137 var recordIndex = UserIndexToRecordIndex(index); 138 return new Record(this, recordIndex, GetRecord(recordIndex)); 139 } 140 set 141 { 142 if (index < 0 || index >= m_RecordCount) 143 throw new ArgumentOutOfRangeException( 144 $"Index {index} is out of range for history with {m_RecordCount} entries", nameof(index)); 145 146 var recordIndex = UserIndexToRecordIndex(index); 147 new Record(this, recordIndex, GetRecord(recordIndex)).CopyFrom(value); 148 } 149 } 150 151 public Action<Record> onRecordAdded { get; set; } 152 public Func<InputControl, double, InputEventPtr, bool> onShouldRecordStateChange { get; set; } 153 154 public InputStateHistory(int maxStateSizeInBytes) 155 { 156 if (maxStateSizeInBytes <= 0) 157 throw new ArgumentException("State size must be >= 0", nameof(maxStateSizeInBytes)); 158 159 m_AddNewControls = true; 160 m_StateSizeInBytes = maxStateSizeInBytes.AlignToMultipleOf(4); 161 } 162 163 public InputStateHistory(string path) 164 { 165 using (var controls = InputSystem.FindControls(path)) 166 { 167 m_Controls = controls.ToArray(); 168 m_ControlCount = m_Controls.Length; 169 } 170 } 171 172 public InputStateHistory(InputControl control) 173 { 174 if (control == null) 175 throw new ArgumentNullException(nameof(control)); 176 177 m_Controls = new[] {control}; 178 m_ControlCount = 1; 179 } 180 181 public InputStateHistory(IEnumerable<InputControl> controls) 182 { 183 if (controls != null) 184 { 185 m_Controls = controls.ToArray(); 186 m_ControlCount = m_Controls.Length; 187 } 188 } 189 190 ~InputStateHistory() 191 { 192 Dispose(); 193 } 194 195 public void Clear() 196 { 197 m_HeadIndex = 0; 198 m_RecordCount = 0; 199 ++m_CurrentVersion; 200 201 // NOTE: Won't clear controls that have been added on the fly. 202 } 203 204 public unsafe Record AddRecord(Record record) 205 { 206 var recordPtr = AllocateRecord(out var index); 207 var newRecord = new Record(this, index, recordPtr); 208 newRecord.CopyFrom(record); 209 return newRecord; 210 } 211 212 public void StartRecording() 213 { 214 // We defer allocation until we actually get values on a control. 215 216 foreach (var control in controls) 217 InputState.AddChangeMonitor(control, this); 218 } 219 220 public void StopRecording() 221 { 222 foreach (var control in controls) 223 InputState.RemoveChangeMonitor(control, this); 224 } 225 226 public unsafe Record RecordStateChange(InputControl control, InputEventPtr eventPtr) 227 { 228 if (eventPtr.IsA<DeltaStateEvent>()) 229 throw new NotImplementedException(); 230 231 if (!eventPtr.IsA<StateEvent>()) 232 throw new ArgumentException($"Event must be a state event but is '{eventPtr}' instead", 233 nameof(eventPtr)); 234 235 var statePtr = (byte*)StateEvent.From(eventPtr)->state - control.device.stateBlock.byteOffset; 236 return RecordStateChange(control, statePtr, eventPtr.time); 237 } 238 239 public unsafe Record RecordStateChange(InputControl control, void* statePtr, double time) 240 { 241 var controlIndex = ArrayHelpers.IndexOfReference(m_Controls, control, m_ControlCount); 242 if (controlIndex == -1) 243 { 244 if (m_AddNewControls) 245 { 246 if (control.stateBlock.alignedSizeInBytes > m_StateSizeInBytes) 247 throw new InvalidOperationException( 248 $"Cannot add control '{control}' with state larger than {m_StateSizeInBytes} bytes"); 249 controlIndex = ArrayHelpers.AppendWithCapacity(ref m_Controls, ref m_ControlCount, control); 250 } 251 else 252 throw new ArgumentException($"Control '{control}' is not part of InputStateHistory", 253 nameof(control)); 254 } 255 256 var recordPtr = AllocateRecord(out var index); 257 recordPtr->time = time; 258 recordPtr->version = ++m_CurrentVersion; 259 var stateBufferPtr = recordPtr->statePtrWithoutControlIndex; 260 if (m_ControlCount > 1 || m_AddNewControls) 261 { 262 // If there's multiple controls, write index of control to which the state change 263 // pertains as an int before the state memory contents following it. 264 recordPtr->controlIndex = controlIndex; 265 stateBufferPtr = recordPtr->statePtrWithControlIndex; 266 } 267 268 var stateSize = control.stateBlock.alignedSizeInBytes; 269 var stateOffset = control.stateBlock.byteOffset; 270 271 UnsafeUtility.MemCpy(stateBufferPtr, (byte*)statePtr + stateOffset, stateSize); 272 273 // Trigger callback. 274 var record = new Record(this, index, recordPtr); 275 onRecordAdded?.Invoke(record); 276 277 return record; 278 } 279 280 public IEnumerator<Record> GetEnumerator() 281 { 282 return new Enumerator(this); 283 } 284 285 IEnumerator IEnumerable.GetEnumerator() 286 { 287 return GetEnumerator(); 288 } 289 290 public void Dispose() 291 { 292 StopRecording(); 293 Destroy(); 294 GC.SuppressFinalize(this); 295 } 296 297 protected void Destroy() 298 { 299 if (m_RecordBuffer.IsCreated) 300 { 301 m_RecordBuffer.Dispose(); 302 m_RecordBuffer = new NativeArray<byte>(); 303 } 304 } 305 306 private void Allocate() 307 { 308 // Find max size of state. 309 if (!m_AddNewControls) 310 { 311 m_StateSizeInBytes = 0; 312 foreach (var control in controls) 313 m_StateSizeInBytes = (int)Math.Max((uint)m_StateSizeInBytes, control.stateBlock.alignedSizeInBytes); 314 } 315 else 316 { 317 Debug.Assert(m_StateSizeInBytes > 0, "State size must be have initialized!"); 318 } 319 320 // Allocate historyDepth times state blocks of the given max size. For each one 321 // add space for the RecordHeader header. 322 // NOTE: If we only have a single control, we omit storing the integer control index. 323 var totalSizeOfBuffer = bytesPerRecord * m_HistoryDepth; 324 m_RecordBuffer = new NativeArray<byte>(totalSizeOfBuffer, Allocator.Persistent, 325 NativeArrayOptions.UninitializedMemory); 326 } 327 328 protected internal int RecordIndexToUserIndex(int index) 329 { 330 if (index < m_HeadIndex) 331 return m_HistoryDepth - m_HeadIndex + index; 332 return index - m_HeadIndex; 333 } 334 335 protected internal int UserIndexToRecordIndex(int index) 336 { 337 return (m_HeadIndex + index) % m_HistoryDepth; 338 } 339 340 protected internal unsafe RecordHeader* GetRecord(int index) 341 { 342 if (!m_RecordBuffer.IsCreated) 343 throw new InvalidOperationException("History buffer has been disposed"); 344 if (index < 0 || index >= m_HistoryDepth) 345 throw new ArgumentOutOfRangeException(nameof(index)); 346 return GetRecordUnchecked(index); 347 } 348 349 internal unsafe RecordHeader* GetRecordUnchecked(int index) 350 { 351 return (RecordHeader*)((byte*)m_RecordBuffer.GetUnsafePtr() + index * bytesPerRecord); 352 } 353 354 protected internal unsafe RecordHeader* AllocateRecord(out int index) 355 { 356 if (!m_RecordBuffer.IsCreated) 357 Allocate(); 358 359 index = (m_HeadIndex + m_RecordCount) % m_HistoryDepth; 360 361 // If we're full, advance head to make room. 362 if (m_RecordCount == m_HistoryDepth) 363 m_HeadIndex = (m_HeadIndex + 1) % m_HistoryDepth; 364 else 365 { 366 // We have a fixed max size given by the history depth and will start overwriting 367 // older entries once we reached max size. 368 ++m_RecordCount; 369 } 370 371 return (RecordHeader*)((byte*)m_RecordBuffer.GetUnsafePtr() + bytesPerRecord * index); 372 } 373 374 protected unsafe TValue ReadValue<TValue>(RecordHeader* data) 375 where TValue : struct 376 { 377 // Get control. If we only have a single one, the index isn't stored on the data. 378 var haveSingleControl = m_ControlCount == 1 && !m_AddNewControls; 379 var control = haveSingleControl ? controls[0] : controls[data->controlIndex]; 380 if (!(control is InputControl<TValue> controlOfType)) 381 throw new InvalidOperationException( 382 $"Cannot read value of type '{TypeHelpers.GetNiceTypeName(typeof(TValue))}' from control '{control}' with value type '{TypeHelpers.GetNiceTypeName(control.valueType)}'"); 383 384 // Grab state memory. 385 var statePtr = haveSingleControl ? data->statePtrWithoutControlIndex : data->statePtrWithControlIndex; 386 statePtr -= control.stateBlock.byteOffset; 387 return controlOfType.ReadValueFromState(statePtr); 388 } 389 390 protected unsafe object ReadValueAsObject(RecordHeader* data) 391 { 392 // Get control. If we only have a single one, the index isn't stored on the data. 393 var haveSingleControl = m_ControlCount == 1 && !m_AddNewControls; 394 var control = haveSingleControl ? controls[0] : controls[data->controlIndex]; 395 396 // Grab state memory. 397 var statePtr = haveSingleControl ? data->statePtrWithoutControlIndex : data->statePtrWithControlIndex; 398 statePtr -= control.stateBlock.byteOffset; 399 return control.ReadValueFromStateAsObject(statePtr); 400 } 401 402 unsafe void IInputStateChangeMonitor.NotifyControlStateChanged(InputControl control, double time, 403 InputEventPtr eventPtr, long monitorIndex) 404 { 405 // Ignore state change if it's in an input update we're not interested in. 406 var currentUpdateType = InputState.currentUpdateType; 407 var updateTypeMask = updateMask; 408 if ((currentUpdateType & updateTypeMask) == 0) 409 return; 410 411 // Ignore state change if we have a filter and the state change doesn't pass the check. 412 if (onShouldRecordStateChange != null && !onShouldRecordStateChange(control, time, eventPtr)) 413 return; 414 415 RecordStateChange(control, control.currentStatePtr, time); 416 } 417 418 // Unused. 419 void IInputStateChangeMonitor.NotifyTimerExpired(InputControl control, double time, long monitorIndex, 420 int timerIndex) 421 { 422 } 423 424 internal InputControl[] m_Controls; 425 internal int m_ControlCount; 426 private NativeArray<byte> m_RecordBuffer; 427 private int m_StateSizeInBytes; 428 private int m_RecordCount; 429 private int m_HistoryDepth = kDefaultHistorySize; 430 private int m_ExtraMemoryPerRecord; 431 internal int m_HeadIndex; 432 internal uint m_CurrentVersion; 433 private InputUpdateType? m_UpdateMask; 434 internal readonly bool m_AddNewControls; 435 436 internal int bytesPerRecord => 437 (m_StateSizeInBytes + 438 m_ExtraMemoryPerRecord + 439 (m_ControlCount == 1 && !m_AddNewControls 440 ? RecordHeader.kSizeWithoutControlIndex 441 : RecordHeader.kSizeWithControlIndex)).AlignToMultipleOf(4); 442 443 private struct Enumerator : IEnumerator<Record> 444 { 445 private readonly InputStateHistory m_History; 446 private int m_Index; 447 448 public Enumerator(InputStateHistory history) 449 { 450 m_History = history; 451 m_Index = -1; 452 } 453 454 public bool MoveNext() 455 { 456 if (m_Index + 1 >= m_History.Count) 457 return false; 458 ++m_Index; 459 return true; 460 } 461 462 public void Reset() 463 { 464 m_Index = -1; 465 } 466 467 public Record Current => m_History[m_Index]; 468 469 object IEnumerator.Current => Current; 470 471 public void Dispose() 472 { 473 } 474 } 475 476 [StructLayout(LayoutKind.Explicit)] 477 protected internal unsafe struct RecordHeader 478 { 479 [FieldOffset(0)] public double time; 480 [FieldOffset(8)] public uint version; 481 [FieldOffset(12)] public int controlIndex; 482 483 [FieldOffset(12)] private fixed byte m_StateWithoutControlIndex[1]; 484 [FieldOffset(16)] private fixed byte m_StateWithControlIndex[1]; 485 486 public byte* statePtrWithControlIndex 487 { 488 get 489 { 490 fixed(byte* ptr = m_StateWithControlIndex) 491 return ptr; 492 } 493 } 494 495 public byte* statePtrWithoutControlIndex 496 { 497 get 498 { 499 fixed(byte* ptr = m_StateWithoutControlIndex) 500 return ptr; 501 } 502 } 503 504 public const int kSizeWithControlIndex = 16; 505 public const int kSizeWithoutControlIndex = 12; 506 } 507 508 public unsafe struct Record : IEquatable<Record> 509 { 510 // We store an index rather than a direct pointer to make this struct safer to use. 511 private readonly InputStateHistory m_Owner; 512 private readonly int m_IndexPlusOne; // Plus one so that default(int) works for us. 513 private uint m_Version; 514 515 internal RecordHeader* header => m_Owner.GetRecord(recordIndex); 516 internal int recordIndex => m_IndexPlusOne - 1; 517 internal uint version => m_Version; 518 519 public bool valid => m_Owner != default && m_IndexPlusOne != default && header->version == m_Version; 520 521 public InputStateHistory owner => m_Owner; 522 523 public int index 524 { 525 get 526 { 527 CheckValid(); 528 return m_Owner.RecordIndexToUserIndex(recordIndex); 529 } 530 } 531 532 public double time 533 { 534 get 535 { 536 CheckValid(); 537 return header->time; 538 } 539 } 540 541 public InputControl control 542 { 543 get 544 { 545 CheckValid(); 546 var controls = m_Owner.controls; 547 if (controls.Count == 1 && !m_Owner.m_AddNewControls) 548 return controls[0]; 549 return controls[header->controlIndex]; 550 } 551 } 552 553 public Record next 554 { 555 get 556 { 557 CheckValid(); 558 var userIndex = m_Owner.RecordIndexToUserIndex(this.recordIndex); 559 if (userIndex + 1 >= m_Owner.Count) 560 return default; 561 var recordIndex = m_Owner.UserIndexToRecordIndex(userIndex + 1); 562 return new Record(m_Owner, recordIndex, m_Owner.GetRecord(recordIndex)); 563 } 564 } 565 566 public Record previous 567 { 568 get 569 { 570 CheckValid(); 571 var userIndex = m_Owner.RecordIndexToUserIndex(this.recordIndex); 572 if (userIndex - 1 < 0) 573 return default; 574 var recordIndex = m_Owner.UserIndexToRecordIndex(userIndex - 1); 575 return new Record(m_Owner, recordIndex, m_Owner.GetRecord(recordIndex)); 576 } 577 } 578 579 internal Record(InputStateHistory owner, int index, RecordHeader* header) 580 { 581 m_Owner = owner; 582 m_IndexPlusOne = index + 1; 583 m_Version = header->version; 584 } 585 586 public TValue ReadValue<TValue>() 587 where TValue : struct 588 { 589 CheckValid(); 590 return m_Owner.ReadValue<TValue>(header); 591 } 592 593 public object ReadValueAsObject() 594 { 595 CheckValid(); 596 return m_Owner.ReadValueAsObject(header); 597 } 598 599 public void* GetUnsafeMemoryPtr() 600 { 601 CheckValid(); 602 return GetUnsafeMemoryPtrUnchecked(); 603 } 604 605 internal void* GetUnsafeMemoryPtrUnchecked() 606 { 607 if (m_Owner.controls.Count == 1 && !m_Owner.m_AddNewControls) 608 return header->statePtrWithoutControlIndex; 609 return header->statePtrWithControlIndex; 610 } 611 612 public void* GetUnsafeExtraMemoryPtr() 613 { 614 CheckValid(); 615 return GetUnsafeExtraMemoryPtrUnchecked(); 616 } 617 618 internal void* GetUnsafeExtraMemoryPtrUnchecked() 619 { 620 if (m_Owner.extraMemoryPerRecord == 0) 621 throw new InvalidOperationException("No extra memory has been set up for history records; set extraMemoryPerRecord"); 622 return (byte*)header + m_Owner.bytesPerRecord - m_Owner.extraMemoryPerRecord; 623 } 624 625 public void CopyFrom(Record record) 626 { 627 if (!record.valid) 628 throw new ArgumentException("Given history record is not valid", nameof(record)); 629 CheckValid(); 630 631 // Find control. 632 var control = record.control; 633 var controlIndex = m_Owner.controls.IndexOfReference(control); 634 if (controlIndex == -1) 635 { 636 // We haven't found it. Throw if we can't add it. 637 if (!m_Owner.m_AddNewControls) 638 throw new InvalidOperationException($"Control '{record.control}' is not tracked by target history"); 639 640 controlIndex = 641 ArrayHelpers.AppendWithCapacity(ref m_Owner.m_Controls, ref m_Owner.m_ControlCount, control); 642 } 643 644 // Make sure memory sizes match. 645 var numBytesForState = m_Owner.m_StateSizeInBytes; 646 if (numBytesForState != record.m_Owner.m_StateSizeInBytes) 647 throw new InvalidOperationException( 648 $"Cannot copy record from owner with state size '{record.m_Owner.m_StateSizeInBytes}' to owner with state size '{numBytesForState}'"); 649 650 // Copy and update header. 651 var thisRecordPtr = header; 652 var otherRecordPtr = record.header; 653 UnsafeUtility.MemCpy(thisRecordPtr, otherRecordPtr, RecordHeader.kSizeWithoutControlIndex); 654 thisRecordPtr->version = ++m_Owner.m_CurrentVersion; 655 m_Version = thisRecordPtr->version; 656 657 // Copy state. 658 var dstPtr = thisRecordPtr->statePtrWithoutControlIndex; 659 if (m_Owner.controls.Count > 1 || m_Owner.m_AddNewControls) 660 { 661 thisRecordPtr->controlIndex = controlIndex; 662 dstPtr = thisRecordPtr->statePtrWithControlIndex; 663 } 664 var srcPtr = record.m_Owner.m_ControlCount > 1 || record.m_Owner.m_AddNewControls 665 ? otherRecordPtr->statePtrWithControlIndex 666 : otherRecordPtr->statePtrWithoutControlIndex; 667 UnsafeUtility.MemCpy(dstPtr, srcPtr, numBytesForState); 668 669 // Copy extra memory, but only if the size in the source and target 670 // history are identical. 671 var numBytesExtraMemory = m_Owner.m_ExtraMemoryPerRecord; 672 if (numBytesExtraMemory > 0 && numBytesExtraMemory == record.m_Owner.m_ExtraMemoryPerRecord) 673 UnsafeUtility.MemCpy(GetUnsafeExtraMemoryPtr(), record.GetUnsafeExtraMemoryPtr(), 674 numBytesExtraMemory); 675 676 // Notify. 677 m_Owner.onRecordAdded?.Invoke(this); 678 } 679 680 internal void CheckValid() 681 { 682 if (m_Owner == default || m_IndexPlusOne == default) 683 throw new InvalidOperationException("Value not initialized"); 684 ////TODO: need to check whether memory has been disposed 685 if (header->version != m_Version) 686 throw new InvalidOperationException("Record is no longer valid"); 687 } 688 689 public bool Equals(Record other) 690 { 691 return ReferenceEquals(m_Owner, other.m_Owner) && m_IndexPlusOne == other.m_IndexPlusOne && m_Version == other.m_Version; 692 } 693 694 public override bool Equals(object obj) 695 { 696 return obj is Record other && Equals(other); 697 } 698 699 public override int GetHashCode() 700 { 701 unchecked 702 { 703 var hashCode = m_Owner != null ? m_Owner.GetHashCode() : 0; 704 hashCode = (hashCode * 397) ^ m_IndexPlusOne; 705 hashCode = (hashCode * 397) ^ (int)m_Version; 706 return hashCode; 707 } 708 } 709 710 public override string ToString() 711 { 712 if (!valid) 713 return "<Invalid>"; 714 715 return $"{{ control={control} value={ReadValueAsObject()} time={time} }}"; 716 } 717 } 718 } 719 720 /// <summary> 721 /// Records value changes of a given control over time. 722 /// </summary> 723 /// <typeparam name="TValue"></typeparam> 724 public class InputStateHistory<TValue> : InputStateHistory, IReadOnlyList<InputStateHistory<TValue>.Record> 725 where TValue : struct 726 { 727 public InputStateHistory(int? maxStateSizeInBytes = null) 728 // Using the size of the value here isn't quite correct but the value is used as an upper 729 // bound on stored state size for which the size of the value should be a reasonable guess. 730 : base(maxStateSizeInBytes ?? UnsafeUtility.SizeOf<TValue>()) 731 { 732 if (maxStateSizeInBytes < UnsafeUtility.SizeOf<TValue>()) 733 throw new ArgumentException("Max state size cannot be smaller than sizeof(TValue)", nameof(maxStateSizeInBytes)); 734 } 735 736 public InputStateHistory(InputControl<TValue> control) 737 : base(control) 738 { 739 } 740 741 public InputStateHistory(string path) 742 : base(path) 743 { 744 // Make sure that the value type of all matched controls is compatible with TValue. 745 foreach (var control in controls) 746 if (!typeof(TValue).IsAssignableFrom(control.valueType)) 747 throw new ArgumentException( 748 $"Control '{control}' matched by '{path}' has value type '{TypeHelpers.GetNiceTypeName(control.valueType)}' which is incompatible with '{TypeHelpers.GetNiceTypeName(typeof(TValue))}'"); 749 } 750 751 ~InputStateHistory() 752 { 753 Destroy(); 754 } 755 756 public unsafe Record AddRecord(Record record) 757 { 758 var recordPtr = AllocateRecord(out var index); 759 var newRecord = new Record(this, index, recordPtr); 760 newRecord.CopyFrom(record); 761 return newRecord; 762 } 763 764 public unsafe Record RecordStateChange(InputControl<TValue> control, TValue value, double time = -1) 765 { 766 using (StateEvent.From(control.device, out var eventPtr)) 767 { 768 var statePtr = (byte*)StateEvent.From(eventPtr)->state - control.device.stateBlock.byteOffset; 769 control.WriteValueIntoState(value, statePtr); 770 if (time >= 0) 771 eventPtr.time = time; 772 var record = RecordStateChange(control, eventPtr); 773 return new Record(this, record.recordIndex, record.header); 774 } 775 } 776 777 public new IEnumerator<Record> GetEnumerator() 778 { 779 return new Enumerator(this); 780 } 781 782 IEnumerator IEnumerable.GetEnumerator() 783 { 784 return GetEnumerator(); 785 } 786 787 public new unsafe Record this[int index] 788 { 789 get 790 { 791 if (index < 0 || index >= Count) 792 throw new ArgumentOutOfRangeException( 793 $"Index {index} is out of range for history with {Count} entries", nameof(index)); 794 795 var recordIndex = UserIndexToRecordIndex(index); 796 return new Record(this, recordIndex, GetRecord(recordIndex)); 797 } 798 set 799 { 800 if (index < 0 || index >= Count) 801 throw new ArgumentOutOfRangeException( 802 $"Index {index} is out of range for history with {Count} entries", nameof(index)); 803 var recordIndex = UserIndexToRecordIndex(index); 804 new Record(this, recordIndex, GetRecord(recordIndex)).CopyFrom(value); 805 } 806 } 807 808 private struct Enumerator : IEnumerator<Record> 809 { 810 private readonly InputStateHistory<TValue> m_History; 811 private int m_Index; 812 813 public Enumerator(InputStateHistory<TValue> history) 814 { 815 m_History = history; 816 m_Index = -1; 817 } 818 819 public bool MoveNext() 820 { 821 if (m_Index + 1 >= m_History.Count) 822 return false; 823 ++m_Index; 824 return true; 825 } 826 827 public void Reset() 828 { 829 m_Index = -1; 830 } 831 832 public Record Current => m_History[m_Index]; 833 834 object IEnumerator.Current => Current; 835 836 public void Dispose() 837 { 838 } 839 } 840 841 public new unsafe struct Record : IEquatable<Record> 842 { 843 private readonly InputStateHistory<TValue> m_Owner; 844 private readonly int m_IndexPlusOne; 845 private uint m_Version; 846 847 internal RecordHeader* header => m_Owner.GetRecord(recordIndex); 848 internal int recordIndex => m_IndexPlusOne - 1; 849 850 public bool valid => m_Owner != default && m_IndexPlusOne != default && header->version == m_Version; 851 852 public InputStateHistory<TValue> owner => m_Owner; 853 854 public int index 855 { 856 get 857 { 858 CheckValid(); 859 return m_Owner.RecordIndexToUserIndex(recordIndex); 860 } 861 } 862 863 public double time 864 { 865 get 866 { 867 CheckValid(); 868 return header->time; 869 } 870 } 871 872 public InputControl<TValue> control 873 { 874 get 875 { 876 CheckValid(); 877 var controls = m_Owner.controls; 878 if (controls.Count == 1 && !m_Owner.m_AddNewControls) 879 return (InputControl<TValue>)controls[0]; 880 return (InputControl<TValue>)controls[header->controlIndex]; 881 } 882 } 883 884 public Record next 885 { 886 get 887 { 888 CheckValid(); 889 var userIndex = m_Owner.RecordIndexToUserIndex(this.recordIndex); 890 if (userIndex + 1 >= m_Owner.Count) 891 return default; 892 var recordIndex = m_Owner.UserIndexToRecordIndex(userIndex + 1); 893 return new Record(m_Owner, recordIndex, m_Owner.GetRecord(recordIndex)); 894 } 895 } 896 897 public Record previous 898 { 899 get 900 { 901 CheckValid(); 902 var userIndex = m_Owner.RecordIndexToUserIndex(this.recordIndex); 903 if (userIndex - 1 < 0) 904 return default; 905 var recordIndex = m_Owner.UserIndexToRecordIndex(userIndex - 1); 906 return new Record(m_Owner, recordIndex, m_Owner.GetRecord(recordIndex)); 907 } 908 } 909 910 internal Record(InputStateHistory<TValue> owner, int index, RecordHeader* header) 911 { 912 m_Owner = owner; 913 m_IndexPlusOne = index + 1; 914 m_Version = header->version; 915 } 916 917 internal Record(InputStateHistory<TValue> owner, int index) 918 { 919 m_Owner = owner; 920 m_IndexPlusOne = index + 1; 921 m_Version = default; 922 } 923 924 public TValue ReadValue() 925 { 926 CheckValid(); 927 return m_Owner.ReadValue<TValue>(header); 928 } 929 930 public void* GetUnsafeMemoryPtr() 931 { 932 CheckValid(); 933 return GetUnsafeMemoryPtrUnchecked(); 934 } 935 936 internal void* GetUnsafeMemoryPtrUnchecked() 937 { 938 if (m_Owner.controls.Count == 1 && !m_Owner.m_AddNewControls) 939 return header->statePtrWithoutControlIndex; 940 return header->statePtrWithControlIndex; 941 } 942 943 public void* GetUnsafeExtraMemoryPtr() 944 { 945 CheckValid(); 946 return GetUnsafeExtraMemoryPtrUnchecked(); 947 } 948 949 internal void* GetUnsafeExtraMemoryPtrUnchecked() 950 { 951 if (m_Owner.extraMemoryPerRecord == 0) 952 throw new InvalidOperationException("No extra memory has been set up for history records; set extraMemoryPerRecord"); 953 return (byte*)header + m_Owner.bytesPerRecord - m_Owner.extraMemoryPerRecord; 954 } 955 956 public void CopyFrom(Record record) 957 { 958 CheckValid(); 959 if (!record.valid) 960 throw new ArgumentException("Given history record is not valid", nameof(record)); 961 var temp = new InputStateHistory.Record(m_Owner, recordIndex, header); 962 temp.CopyFrom(new InputStateHistory.Record(record.m_Owner, record.recordIndex, record.header)); 963 m_Version = temp.version; 964 } 965 966 private void CheckValid() 967 { 968 if (m_Owner == default || m_IndexPlusOne == default) 969 throw new InvalidOperationException("Value not initialized"); 970 if (header->version != m_Version) 971 throw new InvalidOperationException("Record is no longer valid"); 972 } 973 974 public bool Equals(Record other) 975 { 976 return ReferenceEquals(m_Owner, other.m_Owner) && m_IndexPlusOne == other.m_IndexPlusOne && m_Version == other.m_Version; 977 } 978 979 public override bool Equals(object obj) 980 { 981 return obj is Record other && Equals(other); 982 } 983 984 public override int GetHashCode() 985 { 986 unchecked 987 { 988 var hashCode = m_Owner != null ? m_Owner.GetHashCode() : 0; 989 hashCode = (hashCode * 397) ^ m_IndexPlusOne; 990 hashCode = (hashCode * 397) ^ (int)m_Version; 991 return hashCode; 992 } 993 } 994 995 public override string ToString() 996 { 997 if (!valid) 998 return "<Invalid>"; 999 1000 return $"{{ control={control} value={ReadValue()} time={time} }}"; 1001 } 1002 } 1003 } 1004}