A game about forced loneliness, made by TACStudios
1using System;
2using Unity.Collections.LowLevel.Unsafe;
3using UnityEngine.InputSystem.Utilities;
4
5////TODO: method to get raw state pointer for device/control
6
7////REVIEW: allow to restrict state change monitors to specific updates?
8
9namespace UnityEngine.InputSystem.LowLevel
10{
11 using NotifyControlValueChangeAction = Action<InputControl, double, InputEventPtr, long>;
12 using NotifyTimerExpiredAction = Action<InputControl, double, long, int>;
13
14 /// <summary>
15 /// Low-level APIs for working with input state memory.
16 /// </summary>
17 public static class InputState
18 {
19 /// <summary>
20 /// The type of update that was last run or is currently being run on the input state.
21 /// </summary>
22 /// <remarks>
23 /// This determines which set of buffers are currently active and thus determines which view code
24 /// that queries input state will receive. For example, during editor updates, this will be
25 /// <see cref="InputUpdateType.Editor"/> and the state buffers for the editor will be active.
26 /// </remarks>
27 public static InputUpdateType currentUpdateType => InputUpdate.s_LatestUpdateType;
28
29 ////FIXME: ATM this does not work for editor updates
30 /// <summary>
31 /// The number of times the current input state has been updated.
32 /// </summary>
33 public static uint updateCount => InputUpdate.s_UpdateStepCount;
34
35 public static double currentTime => InputRuntime.s_Instance.currentTime - InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;
36
37 /// <summary>
38 /// Callback that is triggered when the state of an input device changes.
39 /// </summary>
40 /// <remarks>
41 /// The first parameter is the device whose state was changed the second parameter is the event
42 /// that triggered the change in state. Note that the latter may be <c>null</c> in case the
43 /// change was performed directly through <see cref="Change"/> rather than through an event.
44 /// </remarks>
45 public static event Action<InputDevice, InputEventPtr> onChange
46 {
47 add => InputSystem.s_Manager.onDeviceStateChange += value;
48 remove => InputSystem.s_Manager.onDeviceStateChange -= value;
49 }
50
51 public static unsafe void Change(InputDevice device, InputEventPtr eventPtr, InputUpdateType updateType = default)
52 {
53 if (device == null)
54 throw new ArgumentNullException(nameof(device));
55 if (!eventPtr.valid)
56 throw new ArgumentNullException(nameof(eventPtr));
57
58 // Make sure event is a StateEvent or DeltaStateEvent and has a format matching the device.
59 FourCC stateFormat;
60 var eventType = eventPtr.type;
61 if (eventType == StateEvent.Type)
62 stateFormat = StateEvent.FromUnchecked(eventPtr)->stateFormat;
63 else if (eventType == DeltaStateEvent.Type)
64 stateFormat = DeltaStateEvent.FromUnchecked(eventPtr)->stateFormat;
65 else
66 {
67 #if UNITY_EDITOR
68 InputSystem.s_Manager.m_Diagnostics?.OnEventFormatMismatch(eventPtr, device);
69 #endif
70 return;
71 }
72
73 if (stateFormat != device.stateBlock.format)
74 throw new ArgumentException(
75 $"State format {stateFormat} from event does not match state format {device.stateBlock.format} of device {device}",
76 nameof(eventPtr));
77
78 InputSystem.s_Manager.UpdateState(device, eventPtr,
79 updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType);
80 }
81
82 /// <summary>
83 /// Perform one update of input state.
84 /// </summary>
85 /// <remarks>
86 /// Incorporates the given state and triggers all state change monitors as needed.
87 ///
88 /// Note that input state changes performed with this method will not be visible on remotes as they will bypass
89 /// event processing. It is effectively equivalent to directly writing into input state memory except that it
90 /// also performs related tasks such as checking state change monitors, flipping buffers, or making the respective
91 /// device current.
92 /// </remarks>
93 public static void Change<TState>(InputControl control, TState state, InputUpdateType updateType = default,
94 InputEventPtr eventPtr = default)
95 where TState : struct
96 {
97 Change(control, ref state, updateType, eventPtr);
98 }
99
100 /// <summary>
101 /// Perform one update of input state.
102 /// </summary>
103 /// <remarks>
104 /// Incorporates the given state and triggers all state change monitors as needed.
105 ///
106 /// Note that input state changes performed with this method will not be visible on remotes as they will bypass
107 /// event processing. It is effectively equivalent to directly writing into input state memory except that it
108 /// also performs related tasks such as checking state change monitors, flipping buffers, or making the respective
109 /// device current.
110 /// </remarks>
111 public static unsafe void Change<TState>(InputControl control, ref TState state, InputUpdateType updateType = default,
112 InputEventPtr eventPtr = default)
113 where TState : struct
114 {
115 if (control == null)
116 throw new ArgumentNullException(nameof(control));
117 if (control.stateBlock.bitOffset != 0 || control.stateBlock.sizeInBits % 8 != 0)
118 throw new ArgumentException($"Cannot change state of bitfield control '{control}' using this method", nameof(control));
119
120 var device = control.device;
121 var stateSize = Math.Min(UnsafeUtility.SizeOf<TState>(), control.m_StateBlock.alignedSizeInBytes);
122 var statePtr = UnsafeUtility.AddressOf(ref state);
123 var stateOffset = control.stateBlock.byteOffset - device.stateBlock.byteOffset;
124
125 InputSystem.s_Manager.UpdateState(device,
126 updateType != default ? updateType : InputSystem.s_Manager.defaultUpdateType, statePtr, stateOffset,
127 (uint)stateSize,
128 eventPtr.valid
129 ? eventPtr.internalTime
130 : InputRuntime.s_Instance.currentTime,
131 eventPtr: eventPtr);
132 }
133
134 public static bool IsIntegerFormat(this FourCC format)
135 {
136 return format == InputStateBlock.FormatBit ||
137 format == InputStateBlock.FormatInt ||
138 format == InputStateBlock.FormatByte ||
139 format == InputStateBlock.FormatShort ||
140 format == InputStateBlock.FormatSBit ||
141 format == InputStateBlock.FormatUInt ||
142 format == InputStateBlock.FormatUShort ||
143 format == InputStateBlock.FormatLong ||
144 format == InputStateBlock.FormatULong;
145 }
146
147 /// <summary>
148 /// Add a monitor that gets triggered every time the state of <paramref name="control"/> changes.
149 /// </summary>
150 /// <param name="control">A control sitting on an <see cref="InputDevice"/> that has been <see cref="InputDevice.added"/>.</param>
151 /// <param name="monitor">Instance of the monitor that should be notified when state changes occur.</param>
152 /// <param name="monitorIndex">Numeric index of the monitors. Monitors on a device are ordered by <em>decreasing</em> monitor index
153 /// and invoked in that order.</param>
154 /// <param name="groupIndex">Numeric group of the monitor. See remarks.</param>
155 /// <exception cref="ArgumentNullException"><paramref name="control"/> is <c>null</c> -or- <paramref name="monitor"/> is <c>null</c>.</exception>
156 /// <exception cref="ArgumentException">The <see cref="InputDevice"/> of <paramref name="control"/> has not been <see cref="InputDevice.added"/>.</exception>
157 /// <remarks>
158 /// All monitors on an <see cref="InputDevice"/> are sorted by the complexity specified in their <paramref name="monitorIndex"/> (in decreasing order) and invoked
159 /// in that order.
160 ///
161 /// Every handler gets an opportunity to set <see cref="InputEventPtr.handled"/> to <c>true</c>. When doing so, all remaining pending monitors
162 /// from the same <paramref name="monitor"/> instance that have the same <paramref name="groupIndex"/> will be silenced and skipped over.
163 /// This can be used to establish an order of event "consumption" where one change monitor may prevent another change monitor from triggering.
164 ///
165 /// Monitors are invoked <em>after</em> a state change has been written to the device. If, for example, a <see cref="StateEvent"/> is
166 /// received that sets <see cref="Gamepad.leftTrigger"/> to <c>0.5</c>, the value is first applied to the control and then any state
167 /// monitors that may be listening to the change are invoked (thus getting <c>0.5</c> if calling <see cref="InputControl{TValue}.ReadValue()"/>).
168 ///
169 /// <example>
170 /// <code>
171 /// class InputMonitor : IInputStateChangeMonitor
172 /// {
173 /// public InputMonitor()
174 /// {
175 /// // Watch the left and right mouse button.
176 /// // By supplying monitor indices here, we not only receive the indices in NotifyControlStateChanged,
177 /// // we also create an ordering between the two monitors. The one on RMB will fire *before* the one
178 /// // on LMB in case there is a single event that changes both buttons.
179 /// InputState.AddChangeMonitor(Mouse.current.leftButton, this, monitorIndex: 1);
180 /// InputState.AddChangeMonitor(Mouse.current.rightButton, this, monitorIndex: 2);
181 /// }
182 ///
183 /// public void NotifyControlStateChanged(InputControl control, double currentTime, InputEventPtr eventPtr, long monitorIndex)
184 /// {
185 /// Debug.Log($"{control} changed");
186 ///
187 /// // We can add a monitor timeout that will trigger in case the state of the
188 /// // given control is not changed within the given time. Let's watch the control
189 /// // for 2 seconds. If nothing happens, we will get a call to NotifyTimerExpired.
190 /// InputState.AddChangeMonitorTimeout(control, this, currentTime + 2);
191 /// }
192 ///
193 /// public void NotifyTimerExpired(InputControl control, double currentTime, long monitorIndex, int timerIndex)
194 /// {
195 /// Debug.Log($"{control} was not changed within 2 seconds");
196 /// }
197 /// }
198 /// </code>
199 /// </example>
200 /// </remarks>
201 public static void AddChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex = -1, uint groupIndex = default)
202 {
203 if (control == null)
204 throw new ArgumentNullException(nameof(control));
205 if (monitor == null)
206 throw new ArgumentNullException(nameof(monitor));
207 if (!control.device.added)
208 throw new ArgumentException($"Device for control '{control}' has not been added to system");
209
210 InputSystem.s_Manager.AddStateChangeMonitor(control, monitor, monitorIndex, groupIndex);
211 }
212
213 public static IInputStateChangeMonitor AddChangeMonitor(InputControl control,
214 NotifyControlValueChangeAction valueChangeCallback, int monitorIndex = -1,
215 NotifyTimerExpiredAction timerExpiredCallback = null)
216 {
217 if (valueChangeCallback == null)
218 throw new ArgumentNullException(nameof(valueChangeCallback));
219 var monitor = new StateChangeMonitorDelegate
220 {
221 valueChangeCallback = valueChangeCallback,
222 timerExpiredCallback = timerExpiredCallback
223 };
224 AddChangeMonitor(control, monitor, monitorIndex);
225 return monitor;
226 }
227
228 public static void RemoveChangeMonitor(InputControl control, IInputStateChangeMonitor monitor, long monitorIndex = -1)
229 {
230 if (control == null)
231 throw new ArgumentNullException(nameof(control));
232 if (monitor == null)
233 throw new ArgumentNullException(nameof(monitor));
234
235 InputSystem.s_Manager.RemoveStateChangeMonitor(control, monitor, monitorIndex);
236 }
237
238 /// <summary>
239 /// Put a timeout on a previously registered state change monitor.
240 /// </summary>
241 /// <param name="control"></param>
242 /// <param name="monitor"></param>
243 /// <param name="time"></param>
244 /// <param name="monitorIndex"></param>
245 /// <param name="timerIndex"></param>
246 /// <remarks>
247 /// If by the given <paramref name="time"/>, no state change has been registered on the control monitored
248 /// by the given <paramref name="monitor">state change monitor</paramref>, <see cref="IInputStateChangeMonitor.NotifyTimerExpired"/>
249 /// will be called on <paramref name="monitor"/>.
250 /// </remarks>
251 public static void AddChangeMonitorTimeout(InputControl control, IInputStateChangeMonitor monitor, double time, long monitorIndex = -1, int timerIndex = -1)
252 {
253 if (monitor == null)
254 throw new ArgumentNullException(nameof(monitor));
255
256 InputSystem.s_Manager.AddStateChangeMonitorTimeout(control, monitor, time, monitorIndex, timerIndex);
257 }
258
259 public static void RemoveChangeMonitorTimeout(IInputStateChangeMonitor monitor, long monitorIndex = -1, int timerIndex = -1)
260 {
261 if (monitor == null)
262 throw new ArgumentNullException(nameof(monitor));
263
264 InputSystem.s_Manager.RemoveStateChangeMonitorTimeout(monitor, monitorIndex, timerIndex);
265 }
266
267 private class StateChangeMonitorDelegate : IInputStateChangeMonitor
268 {
269 public NotifyControlValueChangeAction valueChangeCallback;
270 public NotifyTimerExpiredAction timerExpiredCallback;
271
272 public void NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex)
273 {
274 valueChangeCallback(control, time, eventPtr, monitorIndex);
275 }
276
277 public void NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex)
278 {
279 timerExpiredCallback?.Invoke(control, time, monitorIndex, timerIndex);
280 }
281 }
282 }
283}