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&lt;MyDevice&gt;(); 45 /// } 46 /// 47 /// [Test] 48 /// public void CanCreateMyDevice() 49 /// { 50 /// InputSystem.AddDevice&lt;MyDevice&gt;(); 51 /// Assert.That(InputSystem.devices, Has.Exactly(1).TypeOf&lt;MyDevice&gt;()); 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&lt;ButtonControl&gt;(device, "button", 1); 504 /// Set&lt;AxisControl&gt;(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&lt;Gamepad&gt;(); 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}