A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Reflection;
5using UnityEngine.InputSystem.Controls;
6using NUnit.Framework;
7using NUnit.Framework.Constraints;
8using NUnit.Framework.Internal;
9using Unity.Collections;
10using UnityEngine.InputSystem.LowLevel;
11using UnityEngine.InputSystem.Utilities;
12using UnityEngine.TestTools;
13using UnityEngine.TestTools.Utils;
14#if UNITY_EDITOR
15using UnityEditor;
16using UnityEngine.InputSystem.Editor;
17#endif
18
19////TODO: must allow running UnityTests which means we have to be able to get per-frame updates yet not receive input from native
20
21////TODO: when running tests in players, make sure that remoting is turned off
22
23////REVIEW: always enable event diagnostics in InputTestFixture?
24
25namespace UnityEngine.InputSystem
26{
27 /// <summary>
28 /// A test fixture for writing tests that use the input system. Can be derived from
29 /// or simply instantiated from another test fixture.
30 /// </summary>
31 /// <remarks>
32 /// The fixture will put the input system into a known state where it has only the
33 /// built-in set of basic layouts and no devices. The state of the system before
34 /// starting a test is recorded and restored when the test finishes.
35 ///
36 /// <example>
37 /// <code>
38 /// public class MyInputTests : InputTestFixture
39 /// {
40 /// public override void Setup()
41 /// {
42 /// base.Setup();
43 ///
44 /// InputSystem.RegisterLayout<MyDevice>();
45 /// }
46 ///
47 /// [Test]
48 /// public void CanCreateMyDevice()
49 /// {
50 /// InputSystem.AddDevice<MyDevice>();
51 /// Assert.That(InputSystem.devices, Has.Exactly(1).TypeOf<MyDevice>());
52 /// }
53 /// }
54 /// </code>
55 /// </example>
56 ///
57 /// The test fixture will also sever the tie of the input system to the Unity runtime.
58 /// This means that while the test fixture is active, the input system will not receive
59 /// input and device discovery or removal notifications from platform code. This ensures
60 /// that while the test is running, input that may be generated on the machine running
61 /// the test will not infer with it.
62 /// </remarks>
63 public class InputTestFixture
64 {
65 /// <summary>
66 /// Put <see cref="InputSystem"/> into a known state where it only has a basic set of
67 /// layouts and does not have any input devices.
68 /// </summary>
69 /// <remarks>
70 /// If you derive your own test fixture directly from InputTestFixture, this
71 /// method will automatically be called. If you embed InputTestFixture into
72 /// your fixture, you have to explicitly call this method yourself.
73 /// </remarks>
74 /// <seealso cref="TearDown"/>
75 [SetUp]
76 public virtual void Setup()
77 {
78 try
79 {
80 // Apparently, NUnit is reusing instances :(
81 m_KeyInfos = default;
82 m_IsUnityTest = default;
83 m_CurrentTest = default;
84
85 // Disable input debugger so we don't waste time responding to all the
86 // input system activity from the tests.
87 #if UNITY_EDITOR
88 InputDebuggerWindow.Disable();
89 #endif
90
91 runtime = new InputTestRuntime();
92
93 // Push current input system state on stack.
94#if DEVELOPMENT_BUILD || UNITY_EDITOR
95 InputSystem.SaveAndReset(enableRemoting: false, runtime: runtime);
96#endif
97 // Override the editor messing with logic like canRunInBackground and focus and
98 // make it behave like in the player.
99 #if UNITY_EDITOR
100 InputSystem.settings.editorInputBehaviorInPlayMode = InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView;
101 #endif
102
103 // For a [UnityTest] play mode test, we don't want editor updates interfering with the test,
104 // so turn them off.
105 #if UNITY_EDITOR
106 if (Application.isPlaying && IsUnityTest())
107 InputSystem.s_Manager.m_UpdateMask &= ~InputUpdateType.Editor;
108 #endif
109
110 // We use native collections in a couple places. We when leak them, we want to know where exactly
111 // the allocation came from so enable full leak detection in tests.
112 NativeLeakDetection.Mode = NativeLeakDetectionMode.EnabledWithStackTrace;
113
114 // For [UnityTest]s, we need to process input in sync with the player loop. However, InputTestRuntime
115 // is divorced from the player loop by virtue of not being tied into NativeInputSystem. Listen
116 // for NativeInputSystem.Update here and trigger input processing in our isolated InputSystem.
117 // This is irrelevant for normal [Test]s but for [UnityTest]s that run over several frames, it's crucial.
118 // NOTE: We're severing the tie the previous InputManager had to NativeInputRuntime here. This means that
119 // device removal events that happen to occur while tests are running will get lost.
120 NativeInputRuntime.instance.onUpdate =
121 (InputUpdateType updateType, ref InputEventBuffer buffer) =>
122 {
123 if (InputSystem.s_Manager.ShouldRunUpdate(updateType))
124 InputSystem.Update(updateType);
125 // We ignore any input coming from native.
126 buffer.Reset();
127 };
128 NativeInputRuntime.instance.onShouldRunUpdate =
129 updateType => true;
130
131 #if UNITY_EDITOR
132 m_OnPlayModeStateChange = OnPlayModeStateChange;
133 EditorApplication.playModeStateChanged += m_OnPlayModeStateChange;
134 #endif
135
136 // Always want to merge by default
137 InputSystem.settings.disableRedundantEventsMerging = false;
138
139 // Turn on all optimizations and checks
140 InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, true);
141 InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, true);
142 InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, true);
143
144 #if UNITY_EDITOR
145 // Default mock dialogs to avoid unexpected cancellation of standard flows
146 Dialog.InputActionAsset.SetSaveChanges((_) => Dialog.Result.Discard);
147 Dialog.InputActionAsset.SetDiscardUnsavedChanges((_) => Dialog.Result.Discard);
148 Dialog.InputActionAsset.SetCreateAndOverwriteExistingAsset((_) => Dialog.Result.Discard);
149 Dialog.ControlScheme.SetDeleteControlScheme((_) => Dialog.Result.Delete);
150 #endif
151 }
152 catch (Exception exception)
153 {
154 Debug.LogError("Failed to set up input system for test " + TestContext.CurrentContext.Test.Name);
155 Debug.LogException(exception);
156 throw;
157 }
158
159 m_Initialized = true;
160
161 if (InputSystem.devices.Count > 0)
162 Assert.Fail("Input system should not have devices after reset");
163 }
164
165 /// <summary>
166 /// Restore the state of the input system it had when the test was started.
167 /// </summary>
168 /// <seealso cref="Setup"/>
169 [TearDown]
170 public virtual void TearDown()
171 {
172 if (!m_Initialized)
173 return;
174
175 try
176 {
177#if DEVELOPMENT_BUILD || UNITY_EDITOR
178 InputSystem.Restore();
179#endif
180 runtime.Dispose();
181
182 // Unhook from play mode state changes.
183 #if UNITY_EDITOR
184 if (m_OnPlayModeStateChange != null)
185 EditorApplication.playModeStateChanged -= m_OnPlayModeStateChange;
186 #endif
187
188 // Re-enable input debugger.
189 #if UNITY_EDITOR
190 InputDebuggerWindow.Enable();
191 #endif
192
193 #if UNITY_EDITOR
194 // Re-enable dialogs.
195 Dialog.InputActionAsset.SetSaveChanges(null);
196 Dialog.InputActionAsset.SetDiscardUnsavedChanges(null);
197 Dialog.InputActionAsset.SetCreateAndOverwriteExistingAsset(null);
198 Dialog.ControlScheme.SetDeleteControlScheme(null);
199 #endif
200 }
201 catch (Exception exception)
202 {
203 Debug.LogError("Failed to shut down and restore input system after test " + TestContext.CurrentContext.Test.Name);
204 Debug.LogException(exception);
205 throw;
206 }
207
208 m_Initialized = false;
209 }
210
211 private bool? m_IsUnityTest;
212 private Test m_CurrentTest;
213
214 // True if the current test is a [UnityTest].
215 private bool IsUnityTest()
216 {
217 // We cache this value so that any call after the first in a test no
218 // longer allocates GC memory. Otherwise we'll run into trouble with
219 // DoesNotAllocate tests.
220 var test = TestContext.CurrentTestExecutionContext.CurrentTest;
221 if (m_IsUnityTest.HasValue && m_CurrentTest == test)
222 return m_IsUnityTest.Value;
223
224 var className = test.ClassName;
225 var methodName = test.MethodName;
226
227 // Doesn't seem like there's a proper way to get the current test method based on
228 // the information provided by NUnit (see https://github.com/nunit/nunit/issues/3354).
229
230 var type = Type.GetType(className);
231 if (type == null)
232 {
233 foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
234 {
235 type = assembly.GetType(className);
236 if (type != null)
237 break;
238 }
239 }
240
241 if (type == null)
242 {
243 m_IsUnityTest = false;
244 }
245 else
246 {
247 var method = type.GetMethod(methodName);
248 m_IsUnityTest = method?.GetCustomAttribute<UnityTestAttribute>() != null;
249 }
250
251 m_CurrentTest = test;
252 return m_IsUnityTest.Value;
253 }
254
255 #if UNITY_EDITOR
256 private Action<PlayModeStateChange> m_OnPlayModeStateChange;
257 private void OnPlayModeStateChange(PlayModeStateChange change)
258 {
259 if (change == PlayModeStateChange.ExitingPlayMode && m_Initialized)
260 TearDown();
261 }
262
263 #endif
264
265 // ReSharper disable once MemberCanBeProtected.Global
266 public static void AssertButtonPress<TState>(InputDevice device, TState state, params ButtonControl[] buttons)
267 where TState : struct, IInputStateTypeInfo
268 {
269 // Update state.
270 InputSystem.QueueStateEvent(device, state);
271 InputSystem.Update();
272
273 // Now verify that only the buttons we expect to be pressed are pressed.
274 foreach (var control in device.allControls)
275 {
276 if (!(control is ButtonControl controlAsButton))
277 continue;
278
279 var isInList = buttons.Contains(controlAsButton);
280 if (!isInList)
281 Assert.That(controlAsButton.isPressed, Is.False,
282 $"Expected button {controlAsButton} to NOT be pressed");
283 else
284 Assert.That(controlAsButton.isPressed, Is.True,
285 $"Expected button {controlAsButton} to be pressed");
286 }
287 }
288
289 public static void AssertStickValues(StickControl stick, Vector2 stickValue, float up, float down, float left,
290 float right)
291 {
292 Assert.That(stick.ReadUnprocessedValue(), Is.EqualTo(stickValue));
293
294 Assert.That(stick.up.ReadUnprocessedValue(), Is.EqualTo(up).Within(0.0001), "Incorrect 'up' value");
295 Assert.That(stick.down.ReadUnprocessedValue(), Is.EqualTo(down).Within(0.0001), "Incorrect 'down' value");
296 Assert.That(stick.left.ReadUnprocessedValue(), Is.EqualTo(left).Within(0.0001), "Incorrect 'left' value");
297 Assert.That(stick.right.ReadUnprocessedValue(), Is.EqualTo(right).Within(0.0001), "Incorrect 'right' value");
298 }
299
300 private Dictionary<Key, Tuple<string, int>> m_KeyInfos;
301 private bool m_Initialized;
302
303 /// <summary>
304 /// Set <see cref="Keyboard.keyboardLayout"/> of the given keyboard.
305 /// </summary>
306 /// <param name="name">Name of the keyboard layout to switch to.</param>
307 /// <param name="keyboard">Keyboard to switch layout on. If <c>null</c>, <see cref="Keyboard.current"/> is used.</param>
308 /// <exception cref="ArgumentException"><paramref name="keyboard"/> and <see cref="Keyboard.current"/> are both <c>null</c>.</exception>
309 /// <remarks>
310 /// Also queues and immediately processes an <see cref="DeviceConfigurationEvent"/> for the keyboard.
311 /// </remarks>
312 public unsafe void SetKeyboardLayout(string name, Keyboard keyboard = null)
313 {
314 if (keyboard == null)
315 {
316 keyboard = Keyboard.current;
317 if (keyboard == null)
318 throw new ArgumentException("No keyboard has been created and no keyboard has been given", nameof(keyboard));
319 }
320
321 runtime.SetDeviceCommandCallback(keyboard, (id, command) =>
322 {
323 if (id == QueryKeyboardLayoutCommand.Type)
324 {
325 var commandPtr = (QueryKeyboardLayoutCommand*)command;
326 commandPtr->WriteLayoutName(name);
327 return InputDeviceCommand.GenericSuccess;
328 }
329 return InputDeviceCommand.GenericFailure;
330 });
331
332 // Make sure caches on keys are flushed.
333 InputSystem.QueueConfigChangeEvent(Keyboard.current);
334 InputSystem.Update();
335 }
336
337 /// <summary>
338 /// Set the <see cref="InputControl.displayName"/> of <paramref name="key"/> on the current
339 /// <see cref="Keyboard"/> to be <paramref name="displayName"/>.
340 /// </summary>
341 /// <param name="key">Key to set the display name for.</param>
342 /// <param name="displayName">Display name for the key.</param>
343 /// <param name="scanCode">Optional <see cref="KeyControl.scanCode"/> to report for the key.</param>
344 /// <remarks>
345 /// Automatically adds a <see cref="Keyboard"/> if none has been added yet.
346 /// </remarks>
347 public unsafe void SetKeyInfo(Key key, string displayName, int scanCode = 0)
348 {
349 if (Keyboard.current == null)
350 InputSystem.AddDevice<Keyboard>();
351
352 if (m_KeyInfos == null)
353 {
354 m_KeyInfos = new Dictionary<Key, Tuple<string, int>>();
355
356 runtime.SetDeviceCommandCallback(Keyboard.current,
357 (id, commandPtr) =>
358 {
359 if (commandPtr->type == QueryKeyNameCommand.Type)
360 {
361 var keyNameCommand = (QueryKeyNameCommand*)commandPtr;
362
363 if (m_KeyInfos.TryGetValue((Key)keyNameCommand->scanOrKeyCode, out var info))
364 {
365 keyNameCommand->scanOrKeyCode = info.Item2;
366 StringHelpers.WriteStringToBuffer(info.Item1, (IntPtr)keyNameCommand->nameBuffer,
367 QueryKeyNameCommand.kMaxNameLength);
368 }
369
370 return QueryKeyNameCommand.kSize;
371 }
372
373 return InputDeviceCommand.GenericFailure;
374 });
375 }
376
377 m_KeyInfos[key] = new Tuple<string, int>(displayName, scanCode);
378
379 // Make sure caches on keys are flushed.
380 InputSystem.QueueConfigChangeEvent(Keyboard.current);
381 InputSystem.Update();
382 }
383
384 /// <summary>
385 /// Add support for <see cref="QueryCanRunInBackground"/> to <paramref name="device"/> and return
386 /// <paramref name="value"/> as <see cref="QueryCanRunInBackground.canRunInBackground"/>.
387 /// </summary>
388 /// <param name="device"></param>
389 internal unsafe void SetCanRunInBackground(InputDevice device, bool canRunInBackground = true)
390 {
391 runtime.SetDeviceCommandCallback(device, (id, command) =>
392 {
393 if (command->type == QueryCanRunInBackground.Type)
394 {
395 ((QueryCanRunInBackground*)command)->canRunInBackground = canRunInBackground;
396 return InputDeviceCommand.GenericSuccess;
397 }
398 return InputDeviceCommand.GenericFailure;
399 });
400 }
401
402 public ActionConstraint Started(InputAction action, InputControl control = null, double? time = null, object value = null)
403 {
404 return new ActionConstraint(InputActionPhase.Started, action, control, time: time, duration: 0, value: value);
405 }
406
407 public ActionConstraint Started<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null)
408 where TValue : struct
409 {
410 return new ActionConstraint(InputActionPhase.Started, action, control, value, time: time, duration: 0);
411 }
412
413 public ActionConstraint Performed(InputAction action, InputControl control = null, double? time = null, double? duration = null, object value = null)
414 {
415 return new ActionConstraint(InputActionPhase.Performed, action, control, time: time, duration: duration, value: value);
416 }
417
418 public ActionConstraint Performed<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null, double? duration = null)
419 where TValue : struct
420 {
421 return new ActionConstraint(InputActionPhase.Performed, action, control, value, time: time, duration: duration);
422 }
423
424 public ActionConstraint Canceled(InputAction action, InputControl control = null, double? time = null, double? duration = null, object value = null)
425 {
426 return new ActionConstraint(InputActionPhase.Canceled, action, control, time: time, duration: duration, value: value);
427 }
428
429 public ActionConstraint Canceled<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null, double? duration = null)
430 where TValue : struct
431 {
432 return new ActionConstraint(InputActionPhase.Canceled, action, control, value, time: time, duration: duration);
433 }
434
435 public ActionConstraint Started<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null)
436 where TInteraction : IInputInteraction
437 {
438 return new ActionConstraint(InputActionPhase.Started, action, control, interaction: typeof(TInteraction), time: time,
439 duration: 0, value: value);
440 }
441
442 public ActionConstraint Performed<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null)
443 where TInteraction : IInputInteraction
444 {
445 return new ActionConstraint(InputActionPhase.Performed, action, control, interaction: typeof(TInteraction), time: time,
446 duration: duration, value: value);
447 }
448
449 public ActionConstraint Canceled<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null)
450 where TInteraction : IInputInteraction
451 {
452 return new ActionConstraint(InputActionPhase.Canceled, action, control, interaction: typeof(TInteraction), time: time,
453 duration: duration, value: value);
454 }
455
456 ////REVIEW: Should we determine queueEventOnly automatically from whether we're in a UnityTest?
457
458 // ReSharper disable once MemberCanBeProtected.Global
459 public void Press(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
460 {
461 Set(button, 1, time, timeOffset, queueEventOnly: queueEventOnly);
462 }
463
464 // ReSharper disable once MemberCanBeProtected.Global
465 public void Release(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
466 {
467 Set(button, 0, time, timeOffset, queueEventOnly: queueEventOnly);
468 }
469
470 // ReSharper disable once MemberCanBePrivate.Global
471 public void PressAndRelease(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
472 {
473 Press(button, time, timeOffset, queueEventOnly: true); // This one is always just a queue.
474 Release(button, time, timeOffset, queueEventOnly: queueEventOnly);
475 }
476
477 // ReSharper disable once MemberCanBeProtected.Global
478 public void Click(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
479 {
480 PressAndRelease(button, time, timeOffset, queueEventOnly: queueEventOnly);
481 }
482
483 /// <summary>
484 /// Set the control with the given <paramref name="path"/> on <paramref name="device"/> to the given <paramref name="state"/>
485 /// by sending a state event with the value to the device.
486 /// </summary>
487 /// <param name="device">Device on which to find a control.</param>
488 /// <param name="path">Path of the control on the device.</param>
489 /// <param name="state">New state for the control.</param>
490 /// <param name="time">Timestamp to use for the state event. If -1 (default), current time is used (see <see cref="InputTestFixture.currentTime"/>).</param>
491 /// <param name="timeOffset">Offset to apply to the current time. This is an alternative to <paramref name="time"/>. By default, no offset is applied.</param>
492 /// <param name="queueEventOnly">If true, no <see cref="InputSystem.Update"/> will be performed after queueing the event. This will only put
493 /// the state event on the event queue and not do anything else. The default is to call <see cref="InputSystem.Update"/> after queuing the event.
494 /// Note that not issuing an update means the state of the device will not change yet. This may affect subsequent Set/Press/Release/etc calls
495 /// as they will not yet see the state change.
496 ///
497 /// Note that this parameter will be ignored if the test is a <c>[UnityTest]</c>. Multi-frame
498 /// playmode tests will automatically process input as part of the Unity player loop.</param>
499 /// <typeparam name="TValue">Value type of the control.</typeparam>
500 /// <example>
501 /// <code>
502 /// var device = InputSystem.AddDevice("TestDevice");
503 /// Set<ButtonControl>(device, "button", 1);
504 /// Set<AxisControl>(device, "{Primary2DMotion}/x", 123.456f);
505 /// </code>
506 /// </example>
507 public void Set<TValue>(InputDevice device, string path, TValue state, double time = -1, double timeOffset = 0,
508 bool queueEventOnly = false)
509 where TValue : struct
510 {
511 if (device == null)
512 throw new ArgumentNullException(nameof(device));
513 if (string.IsNullOrEmpty(path))
514 throw new ArgumentNullException(nameof(path));
515
516 var control = (InputControl<TValue>)device[path];
517 Set(control, state, time, timeOffset, queueEventOnly);
518 }
519
520 /// <summary>
521 /// Set the control to the given value by sending a state event with the value to the
522 /// control's device.
523 /// </summary>
524 /// <param name="control">An input control on a device that has been added to the system.</param>
525 /// <param name="state">New value for the input control.</param>
526 /// <param name="time">Timestamp to use for the state event. If -1 (default), current time is used (see <see cref="InputTestFixture.currentTime"/>).</param>
527 /// <param name="timeOffset">Offset to apply to the current time. This is an alternative to <paramref name="time"/>. By default, no offset is applied.</param>
528 /// <param name="queueEventOnly">If true, no <see cref="InputSystem.Update"/> will be performed after queueing the event. This will only put
529 /// the state event on the event queue and not do anything else. The default is to call <see cref="InputSystem.Update"/> after queuing the event.
530 /// Note that not issuing an update means the state of the device will not change yet. This may affect subsequent Set/Press/Release/etc calls
531 /// as they will not yet see the state change.
532 ///
533 /// Note that this parameter will be ignored if the test is a <c>[UnityTest]</c>. Multi-frame
534 /// playmode tests will automatically process input as part of the Unity player loop.</param>
535 /// <typeparam name="TValue">Value type of the given control.</typeparam>
536 /// <example>
537 /// <code>
538 /// var gamepad = InputSystem.AddDevice<Gamepad>();
539 /// Set(gamepad.leftButton, 1);
540 /// </code>
541 /// </example>
542 public void Set<TValue>(InputControl<TValue> control, TValue state, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
543 where TValue : struct
544 {
545 if (control == null)
546 throw new ArgumentNullException(nameof(control));
547 if (!control.device.added)
548 throw new ArgumentException(
549 $"Device of control '{control}' has not been added to the system", nameof(control));
550
551 if (IsUnityTest())
552 queueEventOnly = true;
553
554 void SetUpAndQueueEvent(InputEventPtr eventPtr)
555 {
556 eventPtr.time = (time >= 0 ? time : InputState.currentTime) + timeOffset;
557 control.WriteValueIntoEvent(state, eventPtr);
558 InputSystem.QueueEvent(eventPtr);
559 }
560
561 // Touchscreen does not support delta events involving TouchState.
562 if (control is TouchControl)
563 {
564 using (StateEvent.From(control.device, out var eventPtr))
565 SetUpAndQueueEvent(eventPtr);
566 }
567 else
568 {
569 // We use delta state events rather than full state events here to mitigate the following problem:
570 // Grabbing state from the device will preserve the current values of controls covered in the state.
571 // However, running an update may alter the value of one or more of those controls. So with a full
572 // state event, we may be writing outdated data back into the device. For example, in the case of delta
573 // controls which will reset in OnBeforeUpdate().
574 //
575 // Using delta events, we may still grab state outside of just the one control in case we're looking at
576 // bit-addressed controls but at least we can avoid the problem for the majority of controls.
577 using (DeltaStateEvent.From(control, out var eventPtr))
578 SetUpAndQueueEvent(eventPtr);
579 }
580
581 if (!queueEventOnly)
582 InputSystem.Update();
583 }
584
585 public void Move(InputControl<Vector2> positionControl, Vector2 position, Vector2? delta = null, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
586 {
587 Set(positionControl, position, time: time, timeOffset: timeOffset, queueEventOnly: true);
588
589 var deltaControl = (Vector2Control)positionControl.device.TryGetChildControl("delta");
590 if (deltaControl != null)
591 Set(deltaControl, delta ?? position - positionControl.ReadValue(), time: time, timeOffset: timeOffset, queueEventOnly: true);
592
593 if (!queueEventOnly)
594 InputSystem.Update();
595 }
596
597 ////TODO: obsolete this one in 2.0 and use pressure=1 default value
598 public void BeginTouch(int touchId, Vector2 position, bool queueEventOnly = false, Touchscreen screen = null,
599 double time = -1, double timeOffset = 0, byte displayIndex = 0)
600 {
601 SetTouch(touchId, TouchPhase.Began, position, 1, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset, displayIndex: displayIndex);
602 }
603
604 public void BeginTouch(int touchId, Vector2 position, float pressure, bool queueEventOnly = false, Touchscreen screen = null,
605 double time = -1, double timeOffset = 0)
606 {
607 SetTouch(touchId, TouchPhase.Began, position, pressure, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
608 }
609
610 ////TODO: obsolete this one in 2.0 and use pressure=1 default value
611 public void MoveTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
612 Touchscreen screen = null, double time = -1, double timeOffset = 0)
613 {
614 SetTouch(touchId, TouchPhase.Moved, position, 1, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
615 }
616
617 public void MoveTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false,
618 Touchscreen screen = null, double time = -1, double timeOffset = 0)
619 {
620 SetTouch(touchId, TouchPhase.Moved, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
621 }
622
623 ////TODO: obsolete this one in 2.0 and use pressure=1 default value
624 public void EndTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
625 Touchscreen screen = null, double time = -1, double timeOffset = 0, byte displayIndex = 0)
626 {
627 SetTouch(touchId, TouchPhase.Ended, position, 1, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset, displayIndex: displayIndex);
628 }
629
630 public void EndTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false,
631 Touchscreen screen = null, double time = -1, double timeOffset = 0)
632 {
633 SetTouch(touchId, TouchPhase.Ended, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
634 }
635
636 ////TODO: obsolete this one in 2.0 and use pressure=1 default value
637 public void CancelTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
638 Touchscreen screen = null, double time = -1, double timeOffset = 0)
639 {
640 SetTouch(touchId, TouchPhase.Canceled, position, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
641 }
642
643 public void CancelTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false,
644 Touchscreen screen = null, double time = -1, double timeOffset = 0)
645 {
646 SetTouch(touchId, TouchPhase.Canceled, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
647 }
648
649 ////TODO: obsolete this one in 2.0 and use pressure=1 default value
650 public void SetTouch(int touchId, TouchPhase phase, Vector2 position, Vector2 delta = default,
651 bool queueEventOnly = true, Touchscreen screen = null, double time = -1, double timeOffset = 0)
652 {
653 SetTouch(touchId, phase, position, 1, delta: delta, queueEventOnly: queueEventOnly, screen: screen, time: time,
654 timeOffset: timeOffset);
655 }
656
657 public void SetTouch(int touchId, TouchPhase phase, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = true,
658 Touchscreen screen = null, double time = -1, double timeOffset = 0, byte displayIndex = 0)
659 {
660 if (screen == null)
661 {
662 screen = Touchscreen.current;
663 if (screen == null)
664 screen = InputSystem.AddDevice<Touchscreen>();
665 }
666
667 InputSystem.QueueStateEvent(screen, new TouchState
668 {
669 touchId = touchId,
670 phase = phase,
671 position = position,
672 delta = delta,
673 pressure = pressure,
674 displayIndex = displayIndex,
675 }, (time >= 0 ? time : InputState.currentTime) + timeOffset);
676 if (!queueEventOnly)
677 InputSystem.Update();
678 }
679
680 public void Trigger<TValue>(InputAction action, InputControl<TValue> control, TValue value)
681 where TValue : struct
682 {
683 throw new NotImplementedException();
684 }
685
686 /// <summary>
687 /// Perform the input action without having to know what it is bound to.
688 /// </summary>
689 /// <param name="action">An input action that is currently enabled and has controls it is bound to.</param>
690 /// <remarks>
691 /// Blindly triggering an action requires making a few assumptions. Actions are not built to be able to trigger
692 /// without any input. This means that this method has to generate input on a control that the action is bound to.
693 ///
694 /// Note that this method has no understanding of the interactions that may be present on the action and thus
695 /// does not know how they may affect the triggering of the action.
696 /// </remarks>
697 public void Trigger(InputAction action)
698 {
699 if (action == null)
700 throw new ArgumentNullException(nameof(action));
701
702 if (!action.enabled)
703 throw new ArgumentException(
704 $"Action '{action}' must be enabled in order to be able to trigger it", nameof(action));
705
706 var controls = action.controls;
707 if (controls.Count == 0)
708 throw new ArgumentException(
709 $"Action '{action}' must be bound to controls in order to be able to trigger it", nameof(action));
710
711 // See if we have a button we can trigger.
712 for (var i = 0; i < controls.Count; ++i)
713 {
714 if (!(controls[i] is ButtonControl button))
715 continue;
716
717 // Press and release button.
718 Set(button, 1);
719 Set(button, 0);
720
721 return;
722 }
723
724 // See if we have an axis we can slide a bit.
725 for (var i = 0; i < controls.Count; ++i)
726 {
727 if (!(controls[i] is AxisControl axis))
728 continue;
729
730 // We do, so nudge its value a bit.
731 Set(axis, axis.ReadValue() + 0.01f);
732
733 return;
734 }
735
736 ////TODO: support a wider range of controls
737 throw new NotImplementedException();
738 }
739
740 /// <summary>
741 /// The input runtime used during testing.
742 /// </summary>
743 internal InputTestRuntime runtime { get; private set; }
744
745 /// <summary>
746 /// Get or set the current time used by the input system.
747 /// </summary>
748 /// <value>Current time used by the input system.</value>
749 public double currentTime
750 {
751 get => runtime.currentTime - runtime.currentTimeOffsetToRealtimeSinceStartup;
752 set
753 {
754 runtime.currentTime = value + runtime.currentTimeOffsetToRealtimeSinceStartup;
755 runtime.dontAdvanceTimeNextDynamicUpdate = true;
756 }
757 }
758
759 internal float unscaledGameTime
760 {
761 get => runtime.unscaledGameTime;
762 set
763 {
764 runtime.unscaledGameTime = value;
765 runtime.dontAdvanceUnscaledGameTimeNextDynamicUpdate = true;
766 }
767 }
768
769 public class ActionConstraint : Constraint
770 {
771 public InputActionPhase phase { get; set; }
772 public double? time { get; set; }
773 public double? duration { get; set; }
774 public InputAction action { get; set; }
775 public InputControl control { get; set; }
776 public object value { get; set; }
777 public Type interaction { get; set; }
778
779 private readonly List<ActionConstraint> m_AndThen = new List<ActionConstraint>();
780
781 public ActionConstraint(InputActionPhase phase, InputAction action, InputControl control, object value = null, Type interaction = null, double? time = null, double? duration = null)
782 {
783 this.phase = phase;
784 this.time = time;
785 this.duration = duration;
786 this.action = action;
787 this.control = control;
788 this.value = value;
789 this.interaction = interaction;
790
791 var interactionText = string.Empty;
792 if (interaction != null)
793 interactionText = InputInteraction.GetDisplayName(interaction);
794
795 var actionName = action.actionMap != null ? $"{action.actionMap}/{action.name}" : action.name;
796 // Use same text format as InputActionTrace for easier comparison.
797 var description = $"{{ action={actionName} phase={phase}";
798 if (time != null)
799 description += $" time={time}";
800 if (control != null)
801 description += $" control={control}";
802 if (value != null)
803 description += $" value={value}";
804 if (interaction != null)
805 description += $" interaction={interactionText}";
806 if (duration != null)
807 description += $" duration={duration}";
808 description += " }";
809 Description = description;
810 }
811
812 public override ConstraintResult ApplyTo(object actual)
813 {
814 var trace = (InputActionTrace)actual;
815 var actions = trace.ToArray();
816
817 if (actions.Length == 0)
818 return new ConstraintResult(this, actual, false);
819
820 if (!Verify(actions[0]))
821 return new ConstraintResult(this, actual, false);
822
823 var i = 1;
824 foreach (var constraint in m_AndThen)
825 {
826 if (i >= actions.Length || !constraint.Verify(actions[i]))
827 return new ConstraintResult(this, actual, false);
828 ++i;
829 }
830
831 if (i != actions.Length)
832 return new ConstraintResult(this, actual, false);
833
834 return new ConstraintResult(this, actual, true);
835 }
836
837 private bool Verify(InputActionTrace.ActionEventPtr eventPtr)
838 {
839 // NOTE: Using explicit "return false" branches everywhere for easier setting of breakpoints.
840
841 if (eventPtr.action != action ||
842 eventPtr.phase != phase)
843 return false;
844
845 // Check time.
846 if (time != null && !Mathf.Approximately((float)time.Value, (float)eventPtr.time))
847 return false;
848
849 // Check duration.
850 if (duration != null && !Mathf.Approximately((float)duration.Value, (float)eventPtr.duration))
851 return false;
852
853 // Check control.
854 if (control != null && eventPtr.control != control)
855 return false;
856
857 // Check interaction.
858 if (interaction != null && (eventPtr.interaction == null ||
859 !interaction.IsInstanceOfType(eventPtr.interaction)))
860 return false;
861
862 // Check value.
863 if (value != null)
864 {
865 var val = eventPtr.ReadValueAsObject();
866 if (val is float f)
867 {
868 if (!Mathf.Approximately(f, Convert.ToSingle(value)))
869 return false;
870 }
871 else if (val is double d)
872 {
873 if (!Mathf.Approximately((float)d, (float)Convert.ToDouble(value)))
874 return false;
875 }
876 else if (val is Vector2 v2)
877 {
878 if (!Vector2EqualityComparer.Instance.Equals(v2, value.As<Vector2>()))
879 return false;
880 }
881 else if (val is Vector3 v3)
882 {
883 if (!Vector3EqualityComparer.Instance.Equals(v3, value.As<Vector3>()))
884 return false;
885 }
886 else if (!val.Equals(value))
887 return false;
888 }
889
890 return true;
891 }
892
893 public ActionConstraint AndThen(ActionConstraint constraint)
894 {
895 m_AndThen.Add(constraint);
896 Description += " and\n";
897 Description += constraint.Description;
898 return this;
899 }
900 }
901
902 #if UNITY_EDITOR
903 internal void SimulateDomainReload()
904 {
905 // This quite invasively goes into InputSystem internals. Unfortunately, we
906 // have no proper way of simulating domain reloads ATM. So we directly call various
907 // internal methods here in a sequence similar to what we'd get during a domain reload.
908
909 InputSystem.s_SystemObject.OnBeforeSerialize();
910 InputSystem.s_SystemObject = null;
911 InputSystem.InitializeInEditor(runtime);
912 }
913
914 #endif
915
916 #if UNITY_EDITOR
917 /// <summary>
918 /// Represents an analytics registration event captured by test harness.
919 /// </summary>
920 protected struct AnalyticsRegistrationEventData
921 {
922 public AnalyticsRegistrationEventData(string name, int maxPerHour, int maxPropertiesPerEvent)
923 {
924 this.name = name;
925 this.maxPerHour = maxPerHour;
926 this.maxPropertiesPerEvent = maxPropertiesPerEvent;
927 }
928
929 public readonly string name;
930 public readonly int maxPerHour;
931 public readonly int maxPropertiesPerEvent;
932 }
933
934 /// <summary>
935 /// Represents an analytics data event captured by test harness.
936 /// </summary>
937 protected struct AnalyticsEventData
938 {
939 public AnalyticsEventData(string name, object data)
940 {
941 this.name = name;
942 this.data = data;
943 }
944
945 public readonly string name;
946 public readonly object data;
947 }
948
949 private List<AnalyticsRegistrationEventData> m_RegisteredAnalytics;
950 private List<AnalyticsEventData> m_SentAnalyticsEvents;
951
952 /// <summary>
953 /// Returns a read-only list of all analytics events registred by enabling capture via <see cref="CollectAnalytics(System.Predicate{string})"/>.
954 /// </summary>
955 protected IReadOnlyList<AnalyticsRegistrationEventData> registeredAnalytics => m_RegisteredAnalytics;
956
957 /// <summary>
958 /// Returns a read-only list of all analytics events captured by enabling capture via <see cref="CollectAnalytics(System.Predicate{string})"/>.
959 /// </summary>
960 protected IReadOnlyList<AnalyticsEventData> sentAnalyticsEvents => m_SentAnalyticsEvents;
961
962 /// <summary>
963 /// Set up the test fixture to collect analytics registrations and events
964 /// </summary>
965 /// <param name="analyticsNameFilter">A filter predicate evaluating whether the given analytics name should be accepted to be stored in test fixture.</param>
966 protected void CollectAnalytics(Predicate<string> analyticsNameFilter)
967 {
968 // Make sure containers are initialized and create them if not. Otherwise just clear to avoid allocation.
969 if (m_RegisteredAnalytics == null)
970 m_RegisteredAnalytics = new List<AnalyticsRegistrationEventData>();
971 else
972 m_RegisteredAnalytics.Clear();
973 if (m_SentAnalyticsEvents == null)
974 m_SentAnalyticsEvents = new List<AnalyticsEventData>();
975 else
976 m_SentAnalyticsEvents.Clear();
977
978 // Store registered analytics when called if filter applies
979 runtime.onRegisterAnalyticsEvent = (name, maxPerHour, maxPropertiesPerEvent) =>
980 {
981 if (analyticsNameFilter(name))
982 m_RegisteredAnalytics.Add(new AnalyticsRegistrationEventData(name: name, maxPerHour: maxPerHour, maxPropertiesPerEvent: maxPropertiesPerEvent));
983 };
984
985 // Store sent analytic events when called if filter applies
986 runtime.onSendAnalyticsEvent = (name, data) =>
987 {
988 if (analyticsNameFilter(name))
989 m_SentAnalyticsEvents.Add(new AnalyticsEventData(name: name, data: data));
990 };
991 }
992
993 /// <summary>
994 /// Set up the test fixture to collect filtered analytics registrations and events.
995 /// </summary>
996 /// <param name="acceptedName">The analytics name to be accepted, all other registrations and data
997 /// will be discarded.</param>
998 protected void CollectAnalytics(string acceptedName)
999 {
1000 CollectAnalytics((name) => name.Equals(acceptedName));
1001 }
1002
1003 /// <summary>
1004 /// Set up the test fixture to collect ALL analytics registrations and events.
1005 /// </summary>
1006 protected void CollectAnalytics()
1007 {
1008 CollectAnalytics((_) => true);
1009 }
1010
1011 #endif
1012 }
1013}