File size: 15,161 Bytes
18a519f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
using System;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Utilities;

////TODO: method to get raw state pointer for device/control

////REVIEW: allow to restrict state change monitors to specific updates?

namespace UnityEngine.InputSystem.LowLevel
{
    using NotifyControlValueChangeAction = Action<InputControl, double, InputEventPtr, long>;
    using NotifyTimerExpiredAction = Action<InputControl, double, long, int>;

    /// <summary>
    /// Low-level APIs for working with input state memory.
    /// </summary>
    public static class InputState
    {
        /// <summary>
        /// The type of update that was last run or is currently being run on the input state.
        /// </summary>
        /// <remarks>
        /// This determines which set of buffers are currently active and thus determines which view code
        /// that queries input state will receive. For example, during editor updates, this will be
        /// <see cref="InputUpdateType.Editor"/> and the state buffers for the editor will be active.
        /// </remarks>
        public static InputUpdateType currentUpdateType => InputUpdate.s_LatestUpdateType;

        ////FIXME: ATM this does not work for editor updates
        /// <summary>
        /// The number of times the current input state has been updated.
        /// </summary>
        public static uint updateCount => InputUpdate.s_UpdateStepCount;

        public static double currentTime => InputRuntime.s_Instance.currentTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;

        /// <summary>
        /// Callback that is triggered when the state of an input device changes.
        /// </summary>
        /// <remarks>
        /// The first parameter is the device whose state was changed the second parameter is the event
        /// that triggered the change in state. Note that the latter may be <c>null</c> in case the
        /// change was performed directly through <see cref="Change"/> rather than through an event.
        /// </remarks>
        public static event Action<InputDevice, InputEventPtr> onChange
        {
            add => InputSystem.s_Manager.onDeviceStateChange += value;
            remove => InputSystem.s_Manager.onDeviceStateChange -= value;
        }

        public static unsafe void Change(InputDevice device, InputEventPtr eventPtr, InputUpdateType updateType = default)
        {
            if (device == null)
                throw new ArgumentNullException(nameof(device));
            if (!eventPtr.valid)
                throw new ArgumentNullException(nameof(eventPtr));

            // Make sure event is a StateEvent or DeltaStateEvent and has a format matching the device.
            FourCC stateFormat;
            var eventType = eventPtr.type;
            if (eventType == StateEvent.Type)
                stateFormat = StateEvent.FromUnchecked(eventPtr)->stateFormat;
            else if (eventType == DeltaStateEvent.Type)
                stateFormat = DeltaStateEvent.FromUnchecked(eventPtr)->stateFormat;
            else
            {
                #if UNITY_EDITOR
                InputSystem.s_Manager.m_Diagnostics?.OnEventFormatMismatch(eventPtr, device);
                #endif
                return;
            }

            if (stateFormat != device.stateBlock.format)
                throw new ArgumentException(
                    $"State format {stateFormat} from event does not match state format {device.stateBlock.format} of device {device}",
                    nameof(eventPtr));

            InputSystem.s_Manager.UpdateState(device, eventPtr,
                updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType);
        }

        /// <summary>
        /// Perform one update of input state.
        /// </summary>
        /// <remarks>
        /// Incorporates the given state and triggers all state change monitors as needed.
        ///
        /// Note that input state changes performed with this method will not be visible on remotes as they will bypass
        /// event processing. It is effectively equivalent to directly writing into input state memory except that it
        /// also performs related tasks such as checking state change monitors, flipping buffers, or making the respective
        /// device current.
        /// </remarks>
        public static void Change<TState>(InputControl control, TState state, InputUpdateType updateType = default,
            InputEventPtr eventPtr = default)
            where TState : struct
        {
            Change(control, ref state, updateType, eventPtr);
        }

        /// <summary>
        /// Perform one update of input state.
        /// </summary>
        /// <remarks>
        /// Incorporates the given state and triggers all state change monitors as needed.
        ///
        /// Note that input state changes performed with this method will not be visible on remotes as they will bypass
        /// event processing. It is effectively equivalent to directly writing into input state memory except that it
        /// also performs related tasks such as checking state change monitors, flipping buffers, or making the respective
        /// device current.
        /// </remarks>
        public static unsafe void Change<TState>(InputControl control, ref TState state, InputUpdateType updateType = default,
            InputEventPtr eventPtr = default)
            where TState : struct
        {
            if (control == null)
                throw new ArgumentNullException(nameof(control));
            if (control.stateBlock.bitOffset != 0 || control.stateBlock.sizeInBits % 8 != 0)
                throw new ArgumentException($"Cannot change state of bitfield control '{control}' using this method", nameof(control));

            var device = control.device;
            var stateSize = Math.Min(UnsafeUtility.SizeOf<TState>(), control.m_StateBlock.alignedSizeInBytes);
            var statePtr = UnsafeUtility.AddressOf(ref state);
            var stateOffset = control.stateBlock.byteOffset - device.stateBlock.byteOffset;

            InputSystem.s_Manager.UpdateState(device,
                updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType, statePtr, stateOffset,
                (uint)stateSize,
                eventPtr.valid
                ? eventPtr.internalTime
                : InputRuntime.s_Instance.currentTime,
                eventPtr: eventPtr);
        }

        public static bool IsIntegerFormat(this FourCC format)
        {
            return format == InputStateBlock.FormatBit ||
                format == InputStateBlock.FormatInt ||
                format == InputStateBlock.FormatByte ||
                format == InputStateBlock.FormatShort ||
                format == InputStateBlock.FormatSBit ||
                format == InputStateBlock.FormatUInt ||
                format == InputStateBlock.FormatUShort ||
                format == InputStateBlock.FormatLong ||
                format == InputStateBlock.FormatULong;
        }

        /// <summary>
        /// Add a monitor that gets triggered every time the state of <paramref name="control"/> changes.
        /// </summary>
        /// <param name="control">A control sitting on an <see cref="InputDevice"/> that has been <see cref="InputDevice.added"/>.</param>
        /// <param name="monitor">Instance of the monitor that should be notified when state changes occur.</param>
        /// <param name="monitorIndex">Numeric index of the monitors. Monitors on a device are ordered by <em>decreasing</em> monitor index
        /// and invoked in that order.</param>
        /// <param name="groupIndex">Numeric group of the monitor. See remarks.</param>
        /// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c> -or- <paramref name="monitor"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException">The <see cref="InputDevice"/> of <paramref name="control"/> has not been <see cref="InputDevice.added"/>.</exception>
        /// <remarks>
        /// All monitors on an <see cref="InputDevice"/> are sorted by the complexity specified in their <paramref name="monitorIndex"/> (in decreasing order) and invoked
        /// in that order.
        ///
        /// Every handler gets an opportunity to set <see cref="InputEventPtr.handled"/> to <c>true</c>. When doing so, all remaining pending monitors
        /// from the same <paramref name="monitor"/> instance that have the same <paramref name="groupIndex"/> will be silenced and skipped over.
        /// This can be used to establish an order of event "consumption" where one change monitor may prevent another change monitor from triggering.
        ///
        /// Monitors are invoked <em>after</em> a state change has been written to the device. If, for example, a <see cref="StateEvent"/> is
        /// received that sets <see cref="Gamepad.leftTrigger"/> to <c>0.5</c>, the value is first applied to the control and then any state
        /// monitors that may be listening to the change are invoked (thus getting <c>0.5</c> if calling <see cref="InputControl{TValue}.ReadValue()"/>).
        ///
        /// <example>
        /// <code>
        /// class InputMonitor : IInputStateChangeMonitor
        /// {
        ///     public InputMonitor()
        ///     {
        ///         // Watch the left and right mouse button.
        ///         // By supplying monitor indices here, we not only receive the indices in NotifyControlStateChanged,
        ///         // we also create an ordering between the two monitors. The one on RMB will fire *before* the one
        ///         // on LMB in case there is a single event that changes both buttons.
        ///         InputState.AddChangeMonitor(Mouse.current.leftButton, this, monitorIndex: 1);
        ///         InputState.AddChangeMonitor(Mouse.current.rightButton, this, monitorIndex: 2);
        ///     }
        ///
        ///     public void NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex)
        ///     {
        ///         Debug.Log($"{control} changed");
        ///
        ///         // We can add a monitor timeout that will trigger in case the state of the
        ///         // given control is not changed within the given time. Let's watch the control
        ///         // for 2 seconds. If nothing happens, we will get a call to NotifyTimerExpired.
        ///         // If, however, there is a state change, the timeout is automatically removed
        ///         // and we will see a call to NotifyControlStateChanged instead.
        ///         InputState.AddChangeMonitorTimeout(control, this, 2);
        ///     }
        ///
        ///     public void NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex)
        ///     {
        ///         Debug.Log($"{control} was not changed within 2 seconds");
        ///     }
        /// }
        /// </code>
        /// </example>
        /// </remarks>
        public static void AddChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex = -1, uint groupIndex = default)
        {
            if (control == null)
                throw new ArgumentNullException(nameof(control));
            if (monitor == null)
                throw new ArgumentNullException(nameof(monitor));
            if (!control.device.added)
                throw new ArgumentException($"Device for control '{control}' has not been added to system");

            InputSystem.s_Manager.AddStateChangeMonitor(control, monitor, monitorIndex, groupIndex);
        }

        public static IInputStateChangeMonitor AddChangeMonitor(InputControl control,
            NotifyControlValueChangeAction valueChangeCallback, int monitorIndex = -1,
            NotifyTimerExpiredAction timerExpiredCallback = null)
        {
            if (valueChangeCallback == null)
                throw new ArgumentNullException(nameof(valueChangeCallback));
            var monitor = new StateChangeMonitorDelegate
            {
                valueChangeCallback = valueChangeCallback,
                timerExpiredCallback = timerExpiredCallback
            };
            AddChangeMonitor(control, monitor, monitorIndex);
            return monitor;
        }

        public static void RemoveChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex = -1)
        {
            if (control == null)
                throw new ArgumentNullException(nameof(control));
            if (monitor == null)
                throw new ArgumentNullException(nameof(monitor));

            InputSystem.s_Manager.RemoveStateChangeMonitor(control, monitor, monitorIndex);
        }

        /// <summary>
        /// Put a timeout on a previously registered state change monitor.
        /// </summary>
        /// <param name="control"></param>
        /// <param name="monitor"></param>
        /// <param name="time"></param>
        /// <param name="monitorIndex"></param>
        /// <param name="timerIndex"></param>
        /// <remarks>
        /// If by the given <paramref name="time"/>, no state change has been registered on the control monitored
        /// by the given <paramref name="monitor">state change monitor</paramref>, <see cref="IInputStateChangeMonitor.NotifyTimerExpired"/>
        /// will be called on <paramref name="monitor"/>. If a state change happens by the given <paramref name="time"/>,
        /// the monitor is notified as usual and the timer is automatically removed.
        /// </remarks>
        public static void AddChangeMonitorTimeout(InputControl control, IInputStateChangeMonitor monitor, double time, long monitorIndex = -1, int timerIndex = -1)
        {
            if (monitor == null)
                throw new ArgumentNullException(nameof(monitor));

            InputSystem.s_Manager.AddStateChangeMonitorTimeout(control, monitor, time, monitorIndex, timerIndex);
        }

        public static void RemoveChangeMonitorTimeout(IInputStateChangeMonitor monitor, long monitorIndex = -1, int timerIndex = -1)
        {
            if (monitor == null)
                throw new ArgumentNullException(nameof(monitor));

            InputSystem.s_Manager.RemoveStateChangeMonitorTimeout(monitor, monitorIndex, timerIndex);
        }

        private class StateChangeMonitorDelegate : IInputStateChangeMonitor
        {
            public NotifyControlValueChangeAction valueChangeCallback;
            public NotifyTimerExpiredAction timerExpiredCallback;

            public void NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex)
            {
                valueChangeCallback(control, time, eventPtr, monitorIndex);
            }

            public void NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex)
            {
                timerExpiredCallback?.Invoke(control, time, monitorIndex, timerIndex);
            }
        }
    }
}