using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.LowLevel;
////REVIEW: why not switch to this being the default mechanism? seems like this could allow us to also solve
//// the actions-update-when-not-expected problem; plus give us access to easy polling
////REVIEW: should this automatically unsubscribe itself on disposal?
////TODO: make it possible to persist this same way that it should be possible to persist InputEventTrace
////TODO: make this one thread-safe
////TODO: add random access capability
////TODO: protect traces against controls changing configuration (if state layouts change, we're affected)
namespace UnityEngine.InputSystem.Utilities
{
///
/// Records the triggering of actions into a sequence of events that can be replayed at will.
///
///
/// This is an alternate way to the callback-based responses (such as )
/// of input actions. Instead of executing response code right away whenever
/// an action triggers, an event is recorded which can then be queried on demand.
///
/// The recorded data will stay valid even if the bindings on the actions are changed (e.g. by enabling a different
/// set of bindings through altering or or
/// when modifying the paths of bindings altogether). Note, however, that when this happens, a trace will have
/// to make a private copy of the data that stores the binding resolution state. This means that there can be
/// GC allocation spike when reconfiguring actions that have recorded data in traces.
///
///
///
/// var trace = new InputActionTrace();
///
/// // Subscribe trace to single action.
/// // (Use UnsubscribeFrom to unsubscribe)
/// trace.SubscribeTo(myAction);
///
/// // Subscribe trace to entire action map.
/// // (Use UnsubscribeFrom to unsubscribe)
/// trace.SubscribeTo(myActionMap);
///
/// // Subscribe trace to all actions in the system.
/// trace.SubscribeToAll();
///
/// // Record a single triggering of an action.
/// myAction.performed +=
/// ctx =>
/// {
/// if (ctx.ReadValue<float>() > 0.5f)
/// trace.RecordAction(ctx);
/// };
///
/// // Output trace to console.
/// Debug.Log(string.Join(",\n", trace));
///
/// // Walk through all recorded actions and then clear trace.
/// foreach (var record in trace)
/// {
/// Debug.Log($"{record.action} was {record.phase} by control {record.control} at {record.time}");
///
/// // To read out the value, you either have to know the value type or read the
/// // value out as a generic byte buffer. Here we assume that the value type is
/// // float.
///
/// Debug.Log("Value: " + record.ReadValue<float>());
///
/// // An alternative is read the value as an object. In this case, you don't have
/// // to know the value type but there will be a boxed object allocation.
/// Debug.Log("Value: " + record.ReadValueAsObject());
/// }
/// trace.Clear();
///
/// // Unsubscribe trace from everything.
/// trace.UnsubscribeFromAll();
///
/// // Release memory held by trace.
/// trace.Dispose();
///
///
///
///
///
///
///
public sealed class InputActionTrace : IEnumerable, IDisposable
{
////REVIEW: this is of limited use without having access to ActionEvent
///
/// Directly access the underlying raw memory queue.
///
public InputEventBuffer buffer => m_EventBuffer;
///
/// Returns the number of events in the associated event buffer.
///
public int count => m_EventBuffer.eventCount;
///
/// Constructs a new default initialized InputActionTrace.
///
///
/// When you use this constructor, the new InputActionTrace object does not start recording any actions.
/// To record actions, you must explicitly set them up after creating the object.
/// Alternatively, you can use one of the other constructor overloads which begin recording actions immediately.
///
///
///
///
public InputActionTrace()
{
}
///
/// Constructs a new InputActionTrace that records .
///
/// The action to be recorded.
/// Thrown if is null.
public InputActionTrace(InputAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
SubscribeTo(action);
}
///
/// Constructs a new InputActionTrace that records all actions in .
///
/// The action-map containing actions to be recorded.
/// Thrown if is null.
public InputActionTrace(InputActionMap actionMap)
{
if (actionMap == null)
throw new ArgumentNullException(nameof(actionMap));
SubscribeTo(actionMap);
}
///
/// Record any action getting triggered anywhere.
///
///
/// This does not require the trace to actually hook into every single action or action map in the system.
/// Instead, the trace will listen to and automatically record
/// every triggered action.
///
///
///
public void SubscribeToAll()
{
if (m_SubscribedToAll)
return;
HookOnActionChange();
m_SubscribedToAll = true;
// Remove manually created subscriptions.
while (m_SubscribedActions.length > 0)
UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]);
while (m_SubscribedActionMaps.length > 0)
UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]);
}
///
/// Unsubscribes from all actions currently being recorded.
///
///
///
public void UnsubscribeFromAll()
{
// Only unhook from OnActionChange if we don't have any recorded actions. If we do have
// any, we still need the callback to be notified about when binding data changes.
if (count == 0)
UnhookOnActionChange();
m_SubscribedToAll = false;
while (m_SubscribedActions.length > 0)
UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]);
while (m_SubscribedActionMaps.length > 0)
UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]);
}
///
/// Subscribes to .
///
/// The action to be recorded.
///
/// **Note:** This method does not prevent you from subscribing to the same action multiple times.
/// If you subscribe to the same action multiple times, your event buffer will contain duplicate entries.
///
/// If is null.
///
///
public void SubscribeTo(InputAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (m_CallbackDelegate == null)
m_CallbackDelegate = RecordAction;
action.performed += m_CallbackDelegate;
action.started += m_CallbackDelegate;
action.canceled += m_CallbackDelegate;
m_SubscribedActions.AppendWithCapacity(action);
}
///
/// Subscribes to all actions contained within .
///
/// The action-map containing all actions to be recorded.
///
/// **Note:** This method does not prevent you from subscribing to the same action multiple times.
/// If you subscribe to the same action multiple times, your event buffer will contain duplicate entries.
///
/// Thrown if is null.
///
///
public void SubscribeTo(InputActionMap actionMap)
{
if (actionMap == null)
throw new ArgumentNullException(nameof(actionMap));
if (m_CallbackDelegate == null)
m_CallbackDelegate = RecordAction;
actionMap.actionTriggered += m_CallbackDelegate;
m_SubscribedActionMaps.AppendWithCapacity(actionMap);
}
///
/// Unsubscribes from an action, if that action was previously subscribed to.
///
/// The action to unsubscribe from.
///
/// **Note:** This method has no side effects if you attempt to unsubscribe from an action that you have not previously subscribed to.
///
/// Thrown if is null.
///
///
public void UnsubscribeFrom(InputAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (m_CallbackDelegate == null)
return;
action.performed -= m_CallbackDelegate;
action.started -= m_CallbackDelegate;
action.canceled -= m_CallbackDelegate;
var index = m_SubscribedActions.IndexOfReference(action);
if (index != -1)
m_SubscribedActions.RemoveAtWithCapacity(index);
}
///
/// Unsubscribes from all actions included in .
///
/// The action-map containing actions to unsubscribe from.
///
/// **Note:** This method has no side effects if you attempt to unsubscribe from an action-map that you have not previously subscribed to.
///
/// Thrown if is null.
///
///
public void UnsubscribeFrom(InputActionMap actionMap)
{
if (actionMap == null)
throw new ArgumentNullException(nameof(actionMap));
if (m_CallbackDelegate == null)
return;
actionMap.actionTriggered -= m_CallbackDelegate;
var index = m_SubscribedActionMaps.IndexOfReference(actionMap);
if (index != -1)
m_SubscribedActionMaps.RemoveAtWithCapacity(index);
}
///
/// Record the triggering of an action as an action event.
///
///
///
///
///
///
public unsafe void RecordAction(InputAction.CallbackContext context)
{
// Find/add state.
var stateIndex = m_ActionMapStates.IndexOfReference(context.m_State);
if (stateIndex == -1)
stateIndex = m_ActionMapStates.AppendWithCapacity(context.m_State);
// Make sure we get notified if there's a change to binding setups.
HookOnActionChange();
// Allocate event.
var valueSizeInBytes = context.valueSizeInBytes;
var eventPtr =
(ActionEvent*)m_EventBuffer.AllocateEvent(ActionEvent.GetEventSizeWithValueSize(valueSizeInBytes));
// Initialize event.
ref var triggerState = ref context.m_State.actionStates[context.m_ActionIndex];
eventPtr->baseEvent.type = ActionEvent.Type;
eventPtr->baseEvent.time = triggerState.time;
eventPtr->stateIndex = stateIndex;
eventPtr->controlIndex = triggerState.controlIndex;
eventPtr->bindingIndex = triggerState.bindingIndex;
eventPtr->interactionIndex = triggerState.interactionIndex;
eventPtr->startTime = triggerState.startTime;
eventPtr->phase = triggerState.phase;
// Store value.
// NOTE: If the action triggered from a composite, this stores the value as
// read from the composite.
// NOTE: Also, the value we store is a fully processed value.
var valueBuffer = eventPtr->valueData;
context.ReadValue(valueBuffer, valueSizeInBytes);
}
///
/// Clears all recorded data.
///
///
/// **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.
///
public void Clear()
{
m_EventBuffer.Reset();
m_ActionMapStates.ClearWithCapacity();
}
~InputActionTrace()
{
DisposeInternal();
}
///
public override string ToString()
{
if (count == 0)
return "[]";
var str = new StringBuilder();
str.Append('[');
var isFirst = true;
foreach (var eventPtr in this)
{
if (!isFirst)
str.Append(",\n");
str.Append(eventPtr.ToString());
isFirst = false;
}
str.Append(']');
return str.ToString();
}
///
public void Dispose()
{
UnsubscribeFromAll();
DisposeInternal();
}
private void DisposeInternal()
{
// Nuke clones we made of InputActionMapStates.
for (var i = 0; i < m_ActionMapStateClones.length; ++i)
m_ActionMapStateClones[i].Dispose();
m_EventBuffer.Dispose();
m_ActionMapStates.Clear();
m_ActionMapStateClones.Clear();
if (m_ActionChangeDelegate != null)
{
InputSystem.onActionChange -= m_ActionChangeDelegate;
m_ActionChangeDelegate = null;
}
}
///
/// Returns an enumerator that enumerates all action events recorded for this instance.
///
/// Enumerator instance, never null.
///
public IEnumerator GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private bool m_SubscribedToAll;
private bool m_OnActionChangeHooked;
private InlinedArray m_SubscribedActions;
private InlinedArray m_SubscribedActionMaps;
private InputEventBuffer m_EventBuffer;
private InlinedArray m_ActionMapStates;
private InlinedArray m_ActionMapStateClones;
private Action m_CallbackDelegate;
private Action