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<Vector2>("*/<Stick>");
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}