A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Runtime.InteropServices;
5using System.Text;
6using Unity.Collections.LowLevel.Unsafe;
7using UnityEngine.InputSystem.LowLevel;
8
9////REVIEW: why not switch to this being the default mechanism? seems like this could allow us to also solve
10//// the actions-update-when-not-expected problem; plus give us access to easy polling
11
12////REVIEW: should this automatically unsubscribe itself on disposal?
13
14////TODO: make it possible to persist this same way that it should be possible to persist InputEventTrace
15
16////TODO: make this one thread-safe
17
18////TODO: add random access capability
19
20////TODO: protect traces against controls changing configuration (if state layouts change, we're affected)
21
22namespace UnityEngine.InputSystem.Utilities
23{
24 /// <summary>
25 /// Records the triggering of actions into a sequence of events that can be replayed at will.
26 /// </summary>
27 /// <remarks>
28 /// This is an alternate way to the callback-based responses (such as <see cref="InputAction.performed"/>)
29 /// of <see cref="InputAction">input actions</see>. Instead of executing response code right away whenever
30 /// an action triggers, an <see cref="RecordAction">event is recorded</see> which can then be queried on demand.
31 ///
32 /// The recorded data will stay valid even if the bindings on the actions are changed (e.g. by enabling a different
33 /// set of bindings through altering <see cref="InputAction.bindingMask"/> or <see cref="InputActionMap.devices"/> or
34 /// when modifying the paths of bindings altogether). Note, however, that when this happens, a trace will have
35 /// to make a private copy of the data that stores the binding resolution state. This means that there can be
36 /// GC allocation spike when reconfiguring actions that have recorded data in traces.
37 ///
38 /// <example>
39 /// <code>
40 /// var trace = new InputActionTrace();
41 ///
42 /// // Subscribe trace to single action.
43 /// // (Use UnsubscribeFrom to unsubscribe)
44 /// trace.SubscribeTo(myAction);
45 ///
46 /// // Subscribe trace to entire action map.
47 /// // (Use UnsubscribeFrom to unsubscribe)
48 /// trace.SubscribeTo(myActionMap);
49 ///
50 /// // Subscribe trace to all actions in the system.
51 /// trace.SubscribeToAll();
52 ///
53 /// // Record a single triggering of an action.
54 /// myAction.performed +=
55 /// ctx =>
56 /// {
57 /// if (ctx.ReadValue<float>() > 0.5f)
58 /// trace.RecordAction(ctx);
59 /// };
60 ///
61 /// // Output trace to console.
62 /// Debug.Log(string.Join(",\n", trace));
63 ///
64 /// // Walk through all recorded actions and then clear trace.
65 /// foreach (var record in trace)
66 /// {
67 /// Debug.Log($"{record.action} was {record.phase} by control {record.control} at {record.time}");
68 ///
69 /// // To read out the value, you either have to know the value type or read the
70 /// // value out as a generic byte buffer. Here we assume that the value type is
71 /// // float.
72 ///
73 /// Debug.Log("Value: " + record.ReadValue<float>());
74 ///
75 /// // An alternative is read the value as an object. In this case, you don't have
76 /// // to know the value type but there will be a boxed object allocation.
77 /// Debug.Log("Value: " + record.ReadValueAsObject());
78 /// }
79 /// trace.Clear();
80 ///
81 /// // Unsubscribe trace from everything.
82 /// trace.UnsubscribeFromAll();
83 ///
84 /// // Release memory held by trace.
85 /// trace.Dispose();
86 /// </code>
87 /// </example>
88 /// </remarks>
89 /// <seealso cref="InputAction.started"/>
90 /// <seealso cref="InputAction.performed"/>
91 /// <seealso cref="InputAction.canceled"/>
92 /// <seealso cref="InputSystem.onActionChange"/>
93 public sealed class InputActionTrace : IEnumerable<InputActionTrace.ActionEventPtr>, IDisposable
94 {
95 ////REVIEW: this is of limited use without having access to ActionEvent
96 /// <summary>
97 /// Directly access the underlying raw memory queue.
98 /// </summary>
99 public InputEventBuffer buffer => m_EventBuffer;
100
101 /// <summary>
102 /// Returns the number of events in the associated event buffer.
103 /// </summary>
104 public int count => m_EventBuffer.eventCount;
105
106 /// <summary>
107 /// Constructs a new default initialized <c>InputActionTrace</c>.
108 /// </summary>
109 /// <remarks>
110 /// When you use this constructor, the new InputActionTrace object does not start recording any actions.
111 /// To record actions, you must explicitly set them up after creating the object.
112 /// Alternatively, you can use one of the other constructor overloads which begin recording actions immediately.
113 /// </remarks>
114 /// <seealso cref="SubscribeTo(InputAction)"/>
115 /// <seealso cref="SubscribeTo(InputActionMap)"/>
116 /// <seealso cref="SubscribeToAll"/>
117 public InputActionTrace()
118 {
119 }
120
121 /// <summary>
122 /// Constructs a new <c>InputActionTrace</c> that records <paramref name="action"/>.
123 /// </summary>
124 /// <param name="action">The action to be recorded.</param>
125 /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="action"/> is <c>null</c>.</exception>
126 public InputActionTrace(InputAction action)
127 {
128 if (action == null)
129 throw new ArgumentNullException(nameof(action));
130 SubscribeTo(action);
131 }
132
133 /// <summary>
134 /// Constructs a new <c>InputActionTrace</c> that records all actions in <paramref name="actionMap"/>.
135 /// </summary>
136 /// <param name="actionMap">The action-map containing actions to be recorded.</param>
137 /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="action"/> is <c>null</c>.</exception>
138 public InputActionTrace(InputActionMap actionMap)
139 {
140 if (actionMap == null)
141 throw new ArgumentNullException(nameof(actionMap));
142 SubscribeTo(actionMap);
143 }
144
145 /// <summary>
146 /// Record any action getting triggered anywhere.
147 /// </summary>
148 /// <remarks>
149 /// This does not require the trace to actually hook into every single action or action map in the system.
150 /// Instead, the trace will listen to <see cref="InputSystem.onActionChange"/> and automatically record
151 /// every triggered action.
152 /// </remarks>
153 /// <seealso cref="SubscribeTo(InputAction)"/>
154 /// <seealso cref="SubscribeTo(InputActionMap)"/>
155 public void SubscribeToAll()
156 {
157 if (m_SubscribedToAll)
158 return;
159
160 HookOnActionChange();
161 m_SubscribedToAll = true;
162
163 // Remove manually created subscriptions.
164 while (m_SubscribedActions.length > 0)
165 UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]);
166 while (m_SubscribedActionMaps.length > 0)
167 UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]);
168 }
169
170 /// <summary>
171 /// Unsubscribes from all actions currently being recorded.
172 /// </summary>
173 /// <seealso cref="UnsubscribeFrom(InputAction)"/>
174 /// <seealso cref="UnsubscribeFrom(InputActionMap)"/>
175 public void UnsubscribeFromAll()
176 {
177 // Only unhook from OnActionChange if we don't have any recorded actions. If we do have
178 // any, we still need the callback to be notified about when binding data changes.
179 if (count == 0)
180 UnhookOnActionChange();
181
182 m_SubscribedToAll = false;
183
184 while (m_SubscribedActions.length > 0)
185 UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]);
186 while (m_SubscribedActionMaps.length > 0)
187 UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]);
188 }
189
190 /// <summary>
191 /// Subscribes to <paramref name="action"/>.
192 /// </summary>
193 /// <param name="action">The action to be recorded.</param>
194 /// <remarks>
195 /// **Note:** This method does not prevent you from subscribing to the same action multiple times.
196 /// If you subscribe to the same action multiple times, your event buffer will contain duplicate entries.
197 /// </remarks>
198 /// <exception cref="ArgumentNullException">If <paramref name="action"/> is <c>null</c>.</exception>
199 /// <seealso cref="SubscribeTo(InputActionMap)"/>
200 /// <seealso cref="SubscribeToAll"/>
201 public void SubscribeTo(InputAction action)
202 {
203 if (action == null)
204 throw new ArgumentNullException(nameof(action));
205
206 if (m_CallbackDelegate == null)
207 m_CallbackDelegate = RecordAction;
208
209 action.performed += m_CallbackDelegate;
210 action.started += m_CallbackDelegate;
211 action.canceled += m_CallbackDelegate;
212
213 m_SubscribedActions.AppendWithCapacity(action);
214 }
215
216 /// <summary>
217 /// Subscribes to all actions contained within <paramref name="actionMap"/>.
218 /// </summary>
219 /// <param name="actionMap">The action-map containing all actions to be recorded.</param>
220 /// <remarks>
221 /// **Note:** This method does not prevent you from subscribing to the same action multiple times.
222 /// If you subscribe to the same action multiple times, your event buffer will contain duplicate entries.
223 /// </remarks>
224 /// <exception cref="ArgumentNullException">Thrown if <paramref name="actionMap"/> is null.</exception>
225 /// <seealso cref="SubscribeTo(InputAction)"/>
226 /// <seealso cref="SubscribeToAll"/>
227 public void SubscribeTo(InputActionMap actionMap)
228 {
229 if (actionMap == null)
230 throw new ArgumentNullException(nameof(actionMap));
231
232 if (m_CallbackDelegate == null)
233 m_CallbackDelegate = RecordAction;
234
235 actionMap.actionTriggered += m_CallbackDelegate;
236
237 m_SubscribedActionMaps.AppendWithCapacity(actionMap);
238 }
239
240 /// <summary>
241 /// Unsubscribes from an action, if that action was previously subscribed to.
242 /// </summary>
243 /// <param name="action">The action to unsubscribe from.</param>
244 /// <remarks>
245 /// **Note:** This method has no side effects if you attempt to unsubscribe from an action that you have not previously subscribed to.
246 /// </remarks>
247 /// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is <c>null</c>.</exception>
248 /// <seealso cref="UnsubscribeFrom(InputActionMap)"/>
249 /// <seealso cref="UnsubscribeFromAll"/>
250 public void UnsubscribeFrom(InputAction action)
251 {
252 if (action == null)
253 throw new ArgumentNullException(nameof(action));
254
255 if (m_CallbackDelegate == null)
256 return;
257
258 action.performed -= m_CallbackDelegate;
259 action.started -= m_CallbackDelegate;
260 action.canceled -= m_CallbackDelegate;
261
262 var index = m_SubscribedActions.IndexOfReference(action);
263 if (index != -1)
264 m_SubscribedActions.RemoveAtWithCapacity(index);
265 }
266
267 /// <summary>
268 /// Unsubscribes from all actions included in <paramref name="actionMap"/>.
269 /// </summary>
270 /// <param name="actionMap">The action-map containing actions to unsubscribe from.</param>
271 /// <remarks>
272 /// **Note:** This method has no side effects if you attempt to unsubscribe from an action-map that you have not previously subscribed to.
273 /// </remarks>
274 /// <exception cref="ArgumentNullException">Thrown if <paramref name="actionMap"/> is <c>null</c>.</exception>
275 /// <seealso cref="UnsubscribeFrom(InputAction)"/>
276 /// <seealso cref="UnsubscribeFromAll"/>
277 public void UnsubscribeFrom(InputActionMap actionMap)
278 {
279 if (actionMap == null)
280 throw new ArgumentNullException(nameof(actionMap));
281
282 if (m_CallbackDelegate == null)
283 return;
284
285 actionMap.actionTriggered -= m_CallbackDelegate;
286
287 var index = m_SubscribedActionMaps.IndexOfReference(actionMap);
288 if (index != -1)
289 m_SubscribedActionMaps.RemoveAtWithCapacity(index);
290 }
291
292 /// <summary>
293 /// Record the triggering of an action as an <see cref="ActionEventPtr">action event</see>.
294 /// </summary>
295 /// <param name="context"></param>
296 /// <see cref="InputAction.performed"/>
297 /// <see cref="InputAction.started"/>
298 /// <see cref="InputAction.canceled"/>
299 /// <see cref="InputActionMap.actionTriggered"/>
300 public unsafe void RecordAction(InputAction.CallbackContext context)
301 {
302 // Find/add state.
303 var stateIndex = m_ActionMapStates.IndexOfReference(context.m_State);
304 if (stateIndex == -1)
305 stateIndex = m_ActionMapStates.AppendWithCapacity(context.m_State);
306
307 // Make sure we get notified if there's a change to binding setups.
308 HookOnActionChange();
309
310 // Allocate event.
311 var valueSizeInBytes = context.valueSizeInBytes;
312 var eventPtr =
313 (ActionEvent*)m_EventBuffer.AllocateEvent(ActionEvent.GetEventSizeWithValueSize(valueSizeInBytes));
314
315 // Initialize event.
316 ref var triggerState = ref context.m_State.actionStates[context.m_ActionIndex];
317 eventPtr->baseEvent.type = ActionEvent.Type;
318 eventPtr->baseEvent.time = triggerState.time;
319 eventPtr->stateIndex = stateIndex;
320 eventPtr->controlIndex = triggerState.controlIndex;
321 eventPtr->bindingIndex = triggerState.bindingIndex;
322 eventPtr->interactionIndex = triggerState.interactionIndex;
323 eventPtr->startTime = triggerState.startTime;
324 eventPtr->phase = triggerState.phase;
325
326 // Store value.
327 // NOTE: If the action triggered from a composite, this stores the value as
328 // read from the composite.
329 // NOTE: Also, the value we store is a fully processed value.
330 var valueBuffer = eventPtr->valueData;
331 context.ReadValue(valueBuffer, valueSizeInBytes);
332 }
333
334 /// <summary>
335 /// Clears all recorded data.
336 /// </summary>
337 /// <remarks>
338 /// **Note:** This method does not unsubscribe any actions that the instance is listening to, so after clearing the recorded data, new input on those subscribed actions will continue to be recorded.
339 /// </remarks>
340 public void Clear()
341 {
342 m_EventBuffer.Reset();
343 m_ActionMapStates.ClearWithCapacity();
344 }
345
346 ~InputActionTrace()
347 {
348 DisposeInternal();
349 }
350
351 /// <inheritdoc/>
352 public override string ToString()
353 {
354 if (count == 0)
355 return "[]";
356
357 var str = new StringBuilder();
358 str.Append('[');
359 var isFirst = true;
360 foreach (var eventPtr in this)
361 {
362 if (!isFirst)
363 str.Append(",\n");
364 str.Append(eventPtr.ToString());
365 isFirst = false;
366 }
367 str.Append(']');
368 return str.ToString();
369 }
370
371 /// <inheritdoc/>
372 public void Dispose()
373 {
374 UnsubscribeFromAll();
375 DisposeInternal();
376 }
377
378 private void DisposeInternal()
379 {
380 // Nuke clones we made of InputActionMapStates.
381 for (var i = 0; i < m_ActionMapStateClones.length; ++i)
382 m_ActionMapStateClones[i].Dispose();
383
384 m_EventBuffer.Dispose();
385 m_ActionMapStates.Clear();
386 m_ActionMapStateClones.Clear();
387
388 if (m_ActionChangeDelegate != null)
389 {
390 InputSystem.onActionChange -= m_ActionChangeDelegate;
391 m_ActionChangeDelegate = null;
392 }
393 }
394
395 /// <summary>
396 /// Returns an enumerator that enumerates all action events recorded for this instance.
397 /// </summary>
398 /// <returns>Enumerator instance, never <c>null</c>.</returns>
399 /// <seealso cref="ActionEventPtr"/>
400 public IEnumerator<ActionEventPtr> GetEnumerator()
401 {
402 return new Enumerator(this);
403 }
404
405 IEnumerator IEnumerable.GetEnumerator()
406 {
407 return GetEnumerator();
408 }
409
410 private bool m_SubscribedToAll;
411 private bool m_OnActionChangeHooked;
412 private InlinedArray<InputAction> m_SubscribedActions;
413 private InlinedArray<InputActionMap> m_SubscribedActionMaps;
414 private InputEventBuffer m_EventBuffer;
415 private InlinedArray<InputActionState> m_ActionMapStates;
416 private InlinedArray<InputActionState> m_ActionMapStateClones;
417 private Action<InputAction.CallbackContext> m_CallbackDelegate;
418 private Action<object, InputActionChange> m_ActionChangeDelegate;
419
420 private void HookOnActionChange()
421 {
422 if (m_OnActionChangeHooked)
423 return;
424
425 if (m_ActionChangeDelegate == null)
426 m_ActionChangeDelegate = OnActionChange;
427
428 InputSystem.onActionChange += m_ActionChangeDelegate;
429 m_OnActionChangeHooked = true;
430 }
431
432 private void UnhookOnActionChange()
433 {
434 if (!m_OnActionChangeHooked)
435 return;
436
437 InputSystem.onActionChange -= m_ActionChangeDelegate;
438 m_OnActionChangeHooked = false;
439 }
440
441 private void OnActionChange(object actionOrMapOrAsset, InputActionChange change)
442 {
443 // If we're subscribed to all actions, check if an action got triggered.
444 if (m_SubscribedToAll)
445 {
446 switch (change)
447 {
448 case InputActionChange.ActionStarted:
449 case InputActionChange.ActionPerformed:
450 case InputActionChange.ActionCanceled:
451 Debug.Assert(actionOrMapOrAsset is InputAction, "Expected an action");
452 var triggeredAction = (InputAction)actionOrMapOrAsset;
453 var actionIndex = triggeredAction.m_ActionIndexInState;
454 var stateForAction = triggeredAction.m_ActionMap.m_State;
455
456 var context = new InputAction.CallbackContext
457 {
458 m_State = stateForAction,
459 m_ActionIndex = actionIndex,
460 };
461
462 RecordAction(context);
463
464 return;
465 }
466 }
467
468 // We're only interested in changes to the binding resolution state of actions.
469 if (change != InputActionChange.BoundControlsAboutToChange)
470 return;
471
472 // Grab the associated action map(s).
473 if (actionOrMapOrAsset is InputAction action)
474 CloneActionStateBeforeBindingsChange(action.m_ActionMap);
475 else if (actionOrMapOrAsset is InputActionMap actionMap)
476 CloneActionStateBeforeBindingsChange(actionMap);
477 else if (actionOrMapOrAsset is InputActionAsset actionAsset)
478 foreach (var actionMapInAsset in actionAsset.actionMaps)
479 CloneActionStateBeforeBindingsChange(actionMapInAsset);
480 else
481 Debug.Assert(false, "Expected InputAction, InputActionMap or InputActionAsset");
482 }
483
484 private void CloneActionStateBeforeBindingsChange(InputActionMap actionMap)
485 {
486 // Grab the state.
487 var state = actionMap.m_State;
488 if (state == null)
489 {
490 // Bindings have not been resolved yet for this action map. We shouldn't even be
491 // on the notification list in this case, but just in case, ignore.
492 return;
493 }
494
495 // See if we're using the given state.
496 var stateIndex = m_ActionMapStates.IndexOfReference(state);
497 if (stateIndex == -1)
498 return;
499
500 // Yes, we are so make our own private copy of its current state.
501 // NOTE: We do not put these local InputActionMapStates on the global list.
502 var clone = state.Clone();
503 m_ActionMapStateClones.Append(clone);
504 m_ActionMapStates[stateIndex] = clone;
505 }
506
507 /// <summary>
508 /// A wrapper around <see cref="ActionEvent"/> that automatically translates all the
509 /// information in events into their high-level representations.
510 /// </summary>
511 /// <remarks>
512 /// For example, instead of returning <see cref="ActionEvent.controlIndex">control indices</see>,
513 /// it automatically resolves and returns the respective <see cref="InputControl">controls</see>.
514 /// </remarks>
515 public unsafe struct ActionEventPtr
516 {
517 internal InputActionState m_State;
518 internal ActionEvent* m_Ptr;
519
520 /// <summary>
521 /// The <see cref="InputAction"/> associated with this action event.
522 /// </summary>
523 public InputAction action => m_State.GetActionOrNull(m_Ptr->bindingIndex);
524
525 /// <summary>
526 /// The <see cref="InputActionPhase"/> associated with this action event.
527 /// </summary>
528 /// <seealso cref="InputAction.phase"/>
529 /// <seealso cref="InputAction.CallbackContext.phase"/>
530 public InputActionPhase phase => m_Ptr->phase;
531
532 /// <summary>
533 /// The <see cref="InputControl"/> instance associated with this action event.
534 /// </summary>
535 public InputControl control => m_State.controls[m_Ptr->controlIndex];
536
537 /// <summary>
538 /// The <see cref="IInputInteraction"/> instance associated with this action event if applicable, or <c>null</c> if the action event is not associated with an input interaction.
539 /// </summary>
540 public IInputInteraction interaction
541 {
542 get
543 {
544 var index = m_Ptr->interactionIndex;
545 if (index == InputActionState.kInvalidIndex)
546 return null;
547
548 return m_State.interactions[index];
549 }
550 }
551
552 /// <summary>
553 /// The time, in seconds since your game or app started, that the event occurred.
554 /// </summary>
555 /// <remarks>
556 /// Times are in seconds and progress linearly in real-time. The timeline is the same as for <see cref="Time.realtimeSinceStartup"/>.
557 /// </remarks>
558 public double time => m_Ptr->baseEvent.time;
559
560 /// <summary>
561 /// The time, in seconds since your game or app started, that the <see cref="phase"/> transitioned into <see cref="InputActionPhase.Started"/>.
562 /// </summary>
563 public double startTime => m_Ptr->startTime;
564
565 /// <summary>
566 /// The duration, in seconds, that has elapsed between when this event was generated and when the
567 /// action <see cref="phase"/> transitioned to <see cref="InputActionPhase.Started"/> and has remained active.
568 /// </summary>
569 public double duration => time - startTime;
570
571 /// <summary>
572 /// The size, in bytes, of the value associated with this action event.
573 /// </summary>
574 public int valueSizeInBytes => m_Ptr->valueSizeInBytes;
575
576 /// <summary>
577 /// Reads the value associated with this event as an <c>object</c>.
578 /// </summary>
579 /// <returns><c>object</c> representing the value of this action event.</returns>
580 /// <seealso cref="ReadOnlyArray{TValue}"/>
581 /// <seealso cref="ReadValue(void*, int)"/>
582 public object ReadValueAsObject()
583 {
584 if (m_Ptr == null)
585 throw new InvalidOperationException("ActionEventPtr is invalid");
586
587 var valuePtr = m_Ptr->valueData;
588
589 // Check if the value came from a composite.
590 var bindingIndex = m_Ptr->bindingIndex;
591 if (m_State.bindingStates[bindingIndex].isPartOfComposite)
592 {
593 // Yes, so have to put the value/struct data we read into a boxed
594 // object based on the value type of the composite.
595
596 var compositeBindingIndex = m_State.bindingStates[bindingIndex].compositeOrCompositeBindingIndex;
597 var compositeIndex = m_State.bindingStates[compositeBindingIndex].compositeOrCompositeBindingIndex;
598 var composite = m_State.composites[compositeIndex];
599 Debug.Assert(composite != null, "NULL composite instance");
600
601 var valueType = composite.valueType;
602 if (valueType == null)
603 throw new InvalidOperationException($"Cannot read value from Composite '{composite}' which does not have a valueType set");
604
605 return Marshal.PtrToStructure(new IntPtr(valuePtr), valueType);
606 }
607
608 // Expecting action to only trigger from part bindings or bindings outside of composites.
609 Debug.Assert(!m_State.bindingStates[bindingIndex].isComposite, "Action should not have triggered directly from a composite binding");
610
611 // Read value through InputControl.
612 var valueSizeInBytes = m_Ptr->valueSizeInBytes;
613 return control.ReadValueFromBufferAsObject(valuePtr, valueSizeInBytes);
614 }
615
616 /// <summary>
617 /// Reads the value associated with this event into the contiguous memory buffer defined by <c>[buffer, buffer + bufferSize)</c>.
618 /// </summary>
619 /// <param name="buffer">Pointer to the contiguous memory buffer to write value data to.</param>
620 /// <param name="bufferSize">The size, in bytes, of the contiguous buffer pointed to by <paramref name="buffer"/>.</param>
621 /// <exception cref="NullReferenceException">If <paramref name="buffer"/> is <c>null</c>.</exception>
622 /// <exception cref="ArgumentException">If the given <paramref name="bufferSize"/> is less than the number of bytes required to write the event value to <paramref name="buffer"/>.</exception>
623 /// <seealso cref="ReadValueAsObject"/>
624 /// <seealso cref="ReadValue{TValue}"/>
625 public void ReadValue(void* buffer, int bufferSize)
626 {
627 var valueSizeInBytes = m_Ptr->valueSizeInBytes;
628
629 ////REVIEW: do we want more checking than this?
630 if (bufferSize < valueSizeInBytes)
631 throw new ArgumentException(
632 $"Expected buffer of at least {valueSizeInBytes} bytes but got buffer of just {bufferSize} bytes instead",
633 nameof(bufferSize));
634
635 UnsafeUtility.MemCpy(buffer, m_Ptr->valueData, valueSizeInBytes);
636 }
637
638 /// <summary>
639 /// Reads the value associated with this event as an object of type <typeparamref name="TValue"/>.
640 /// </summary>
641 /// <typeparam name="TValue">The event value type to be used.</typeparam>
642 /// <returns>Object of type <typeparamref name="TValue"/>.</returns>
643 /// <exception cref="InvalidOperationException">In case the size of <typeparamref name="TValue"/> does not match the size of the value associated with this event.</exception>
644 public TValue ReadValue<TValue>()
645 where TValue : struct
646 {
647 var valueSizeInBytes = m_Ptr->valueSizeInBytes;
648
649 ////REVIEW: do we want more checking than this?
650 if (UnsafeUtility.SizeOf<TValue>() != valueSizeInBytes)
651 throw new InvalidOperationException(
652 $"Cannot read a value of type '{typeof(TValue).Name}' with size {UnsafeUtility.SizeOf<TValue>()} from event on action '{action}' with value size {valueSizeInBytes}");
653
654 var result = new TValue();
655 var resultPtr = UnsafeUtility.AddressOf(ref result);
656 UnsafeUtility.MemCpy(resultPtr, m_Ptr->valueData, valueSizeInBytes);
657
658 return result;
659 }
660
661 /// <inheritdoc/>
662 public override string ToString()
663 {
664 if (m_Ptr == null)
665 return "<null>";
666
667 var actionName = action.actionMap != null ? $"{action.actionMap.name}/{action.name}" : action.name;
668 return $"{{ action={actionName} phase={phase} time={time} control={control} value={ReadValueAsObject()} interaction={interaction} duration={duration} }}";
669 }
670 }
671
672 private unsafe struct Enumerator : IEnumerator<ActionEventPtr>
673 {
674 private readonly InputActionTrace m_Trace;
675 private readonly ActionEvent* m_Buffer;
676 private readonly int m_EventCount;
677 private ActionEvent* m_CurrentEvent;
678 private int m_CurrentIndex;
679
680 public Enumerator(InputActionTrace trace)
681 {
682 m_Trace = trace;
683 m_Buffer = (ActionEvent*)trace.m_EventBuffer.bufferPtr.data;
684 m_EventCount = trace.m_EventBuffer.eventCount;
685 m_CurrentEvent = null;
686 m_CurrentIndex = 0;
687 }
688
689 public bool MoveNext()
690 {
691 if (m_CurrentIndex == m_EventCount)
692 return false;
693
694 if (m_CurrentEvent == null)
695 {
696 m_CurrentEvent = m_Buffer;
697 return m_CurrentEvent != null;
698 }
699
700 Debug.Assert(m_CurrentEvent != null);
701
702 ++m_CurrentIndex;
703 if (m_CurrentIndex == m_EventCount)
704 return false;
705
706 m_CurrentEvent = (ActionEvent*)InputEvent.GetNextInMemory((InputEvent*)m_CurrentEvent);
707 return true;
708 }
709
710 public void Reset()
711 {
712 m_CurrentEvent = null;
713 m_CurrentIndex = 0;
714 }
715
716 public void Dispose()
717 {
718 }
719
720 public ActionEventPtr Current
721 {
722 get
723 {
724 var state = m_Trace.m_ActionMapStates[m_CurrentEvent->stateIndex];
725 return new ActionEventPtr
726 {
727 m_State = state,
728 m_Ptr = m_CurrentEvent,
729 };
730 }
731 }
732
733 object IEnumerator.Current => Current;
734 }
735 }
736}