Started project

This commit is contained in:
Caleb Sandford deQuincey
2025-11-14 17:30:41 +00:00
parent 00b55a5b1e
commit 334021d04e
36526 changed files with 4940113 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
using System.Reflection;
using System.Runtime.CompilerServices;
// Keep this in sync with "Packages/com.unity.inputsystem/package.json".
// NOTE: Unfortunately, System.Version doesn't use semantic versioning so we can't include
// "-preview" suffixes here.
[assembly: AssemblyVersion("1.14.2")]
[assembly: InternalsVisibleTo("Unity.InputSystem.Tests.Editor")]
[assembly: InternalsVisibleTo("Unity.InputSystem.Tests")]
[assembly: InternalsVisibleTo("Unity.InputSystem.IntegrationTests")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 674b72d5417404b9eac446749e3e2d5e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,216 @@
#if UNITY_EDITOR
using System;
using System.IO;
using UnityEditor;
namespace UnityEngine.InputSystem
{
/// Provides convenience functions for creating and managing assets for test purposes.
/// Note that all returned paths are converted to Unix paths when running on Windows
/// for consistency and to avoid mixed path names.
public static class AssetDatabaseUtils
{
private const string kAssetPath = "Assets";
private const string kTestPath = "TestFiles";
private const string kMetaExtension = ".meta";
private const string kDefaultAssetExtension = "asset";
// Perform an operation equivalent to a file delete operation outside of Unity Editor.
// Note that meta file is also removed to avoid generating warnings about non-clean delete.
public static void ExternalDeleteFileOrDirectory(string path)
{
FileUtil.DeleteFileOrDirectory(path);
FileUtil.DeleteFileOrDirectory(path + kMetaExtension);
}
// Perform an operation equivalent to a file move operation outside of Unity Editor.
// Note that meta file is also moved to avoid generating warnings about non-clean move.
public static void ExternalMoveFileOrDirectory(string source, string dest)
{
FileUtil.MoveFileOrDirectory(source, dest);
FileUtil.MoveFileOrDirectory(source + kMetaExtension, dest + kMetaExtension);
}
// Create an asset at the given path containing the given text content.
private static T CreateAssetAtPath<T>(string path, string content) where T : UnityEngine.Object
{
Debug.Assert(!File.Exists(path));
T obj;
try
{
CreateDirectories(Path.GetDirectoryName(path));
File.WriteAllText(path, content);
AssetDatabase.ImportAsset(path);
obj = AssetDatabase.LoadAssetAtPath<T>(path);
if (obj == null)
throw new Exception($"Failed to create asset at \"{path}\"");
}
catch (Exception)
{
AssetDatabase.DeleteAsset(path);
throw;
}
return obj;
}
private static string SanitizePath(string path)
{
return path?.Replace("\\", "/");
}
private static void CreateRootDirectory()
{
CreateDirectories(RootPath());
}
// Creates all directories (including intermediate) defined in path.
private static string CreateDirectories(string path)
{
if (Directory.Exists(path))
return SanitizePath(path);
var parentFolder = kAssetPath;
path = path.Replace("\\", "/"); // Make sure we only get '/' separators.
var directories = path.Split('/');
if (directories[0] != kAssetPath)
throw new ArgumentException(path);
for (var i = 1; i < directories.Length; ++i)
{
var guid = AssetDatabase.CreateFolder(parentFolder, directories[i]);
if (guid == string.Empty)
throw new Exception("Failed to create path \"" + path + "\"");
parentFolder = SanitizePath(Path.Combine(parentFolder, directories[i]));
}
AssetDatabase.Refresh();
return SanitizePath(path);
}
// Creates a random test directory within asset folder that is automatically removed after test run.
public static string CreateDirectory()
{
return CreateDirectories(RandomDirectoryPath());
}
// Creates an asset in the given directory path with an explicit or random file name containing the
// given content or the default content based on type.
public static T CreateAsset<T>(string directoryPath, string filename = null, string content = null) where T : UnityEngine.Object
{
Debug.Assert(directoryPath == null || directoryPath.Contains(RootPath()));
Debug.Assert(filename == null || !filename.Contains("/"));
if (directoryPath == null)
directoryPath = RootPath();
string path;
if (filename != null)
{
path = SanitizePath(Path.Combine(directoryPath, filename));
if (File.Exists(path))
throw new Exception($"File already exists: {path}");
}
else
{
path = RandomAssetFilePath(directoryPath, AssetFileExtensionFromType(typeof(T)));
}
return CreateAsset<T>(path: path, content: content);
}
// Creates an asset at the given path containing the specified content.
// If path is null, a unique random file name is assigned, if content is null the default content based
// on type (extension) is used.
public static T CreateAsset<T>(string path = null, string content = null) where T : UnityEngine.Object
{
if (path == null)
path = RandomAssetFilePath(RootPath(), AssetFileExtensionFromType(typeof(T)));
if (content == null)
content = DefaultContentFromType(typeof(T));
return CreateAssetAtPath<T>(path, content);
}
public static void Restore()
{
var root = RootPath();
// Delete all files in test folder
if (!Directory.Exists(root))
return;
foreach (var asset in AssetDatabase.FindAssets("", new string[] { root }))
{
var path = AssetDatabase.GUIDToAssetPath(asset);
AssetDatabase.DeleteAsset(path);
}
AssetDatabase.DeleteAsset(root);
}
private static string RandomName()
{
const double scale = int.MaxValue;
double r = UnityEngine.Random.value;
return "Test_" + (int)(Math.Floor(r * scale));
}
private static string RandomAssetFilePath<T>(string directoryPath = null)
{
return RandomAssetFilePath(directoryPath, AssetFileExtensionFromType(typeof(T)));
}
private static string RandomAssetFilePath(string directoryPath = null, string extension = null)
{
// Default to using test files root path
if (directoryPath == null)
directoryPath = RootPath();
// Default to default extension
if (extension == null)
extension = kDefaultAssetExtension;
string path;
do
{
path = SanitizePath(Path.Combine(directoryPath, RandomName() + "." + extension)); // EDIT
}
while (File.Exists(path));
return path;
}
private static string RootPath()
{
return SanitizePath(Path.Combine(kAssetPath, kTestPath));
}
public static string RandomDirectoryPath()
{
string path;
do
{
path = Path.Combine(RootPath(), RandomName());
}
while (File.Exists(path));
return SanitizePath(path);
}
private static string AssetFileExtensionFromType(Type type)
{
if (type == typeof(InputActionAsset))
return InputActionAsset.Extension;
return kDefaultAssetExtension;
}
private static string DefaultContentFromType(Type type)
{
if (type == typeof(InputActionAsset))
return "{}";
return string.Empty;
}
}
}
#endif // UNITY_EDITOR

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a1bc613b5fab4dd4997810700b46dbf7
timeCreated: 1708676790

View File

@@ -0,0 +1,1013 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine.InputSystem.Controls;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using NUnit.Framework.Internal;
using Unity.Collections;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.TestTools;
using UnityEngine.TestTools.Utils;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.InputSystem.Editor;
#endif
////TODO: must allow running UnityTests which means we have to be able to get per-frame updates yet not receive input from native
////TODO: when running tests in players, make sure that remoting is turned off
////REVIEW: always enable event diagnostics in InputTestFixture?
namespace UnityEngine.InputSystem
{
/// <summary>
/// A test fixture for writing tests that use the input system. Can be derived from
/// or simply instantiated from another test fixture.
/// </summary>
/// <remarks>
/// The fixture will put the input system into a known state where it has only the
/// built-in set of basic layouts and no devices. The state of the system before
/// starting a test is recorded and restored when the test finishes.
///
/// <example>
/// <code>
/// public class MyInputTests : InputTestFixture
/// {
/// public override void Setup()
/// {
/// base.Setup();
///
/// InputSystem.RegisterLayout&lt;MyDevice&gt;();
/// }
///
/// [Test]
/// public void CanCreateMyDevice()
/// {
/// InputSystem.AddDevice&lt;MyDevice&gt;();
/// Assert.That(InputSystem.devices, Has.Exactly(1).TypeOf&lt;MyDevice&gt;());
/// }
/// }
/// </code>
/// </example>
///
/// The test fixture will also sever the tie of the input system to the Unity runtime.
/// This means that while the test fixture is active, the input system will not receive
/// input and device discovery or removal notifications from platform code. This ensures
/// that while the test is running, input that may be generated on the machine running
/// the test will not infer with it.
/// </remarks>
public class InputTestFixture
{
/// <summary>
/// Put <see cref="InputSystem"/> into a known state where it only has a basic set of
/// layouts and does not have any input devices.
/// </summary>
/// <remarks>
/// If you derive your own test fixture directly from InputTestFixture, this
/// method will automatically be called. If you embed InputTestFixture into
/// your fixture, you have to explicitly call this method yourself.
/// </remarks>
/// <seealso cref="TearDown"/>
[SetUp]
public virtual void Setup()
{
try
{
// Apparently, NUnit is reusing instances :(
m_KeyInfos = default;
m_IsUnityTest = default;
m_CurrentTest = default;
// Disable input debugger so we don't waste time responding to all the
// input system activity from the tests.
#if UNITY_EDITOR
InputDebuggerWindow.Disable();
#endif
runtime = new InputTestRuntime();
// Push current input system state on stack.
#if DEVELOPMENT_BUILD || UNITY_EDITOR
InputSystem.SaveAndReset(enableRemoting: false, runtime: runtime);
#endif
// Override the editor messing with logic like canRunInBackground and focus and
// make it behave like in the player.
#if UNITY_EDITOR
InputSystem.settings.editorInputBehaviorInPlayMode = InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView;
#endif
// For a [UnityTest] play mode test, we don't want editor updates interfering with the test,
// so turn them off.
#if UNITY_EDITOR
if (Application.isPlaying && IsUnityTest())
InputSystem.s_Manager.m_UpdateMask &= ~InputUpdateType.Editor;
#endif
// We use native collections in a couple places. We when leak them, we want to know where exactly
// the allocation came from so enable full leak detection in tests.
NativeLeakDetection.Mode = NativeLeakDetectionMode.EnabledWithStackTrace;
// For [UnityTest]s, we need to process input in sync with the player loop. However, InputTestRuntime
// is divorced from the player loop by virtue of not being tied into NativeInputSystem. Listen
// for NativeInputSystem.Update here and trigger input processing in our isolated InputSystem.
// This is irrelevant for normal [Test]s but for [UnityTest]s that run over several frames, it's crucial.
// NOTE: We're severing the tie the previous InputManager had to NativeInputRuntime here. This means that
// device removal events that happen to occur while tests are running will get lost.
NativeInputRuntime.instance.onUpdate =
(InputUpdateType updateType, ref InputEventBuffer buffer) =>
{
if (InputSystem.s_Manager.ShouldRunUpdate(updateType))
InputSystem.Update(updateType);
// We ignore any input coming from native.
buffer.Reset();
};
NativeInputRuntime.instance.onShouldRunUpdate =
updateType => true;
#if UNITY_EDITOR
m_OnPlayModeStateChange = OnPlayModeStateChange;
EditorApplication.playModeStateChanged += m_OnPlayModeStateChange;
#endif
// Always want to merge by default
InputSystem.settings.disableRedundantEventsMerging = false;
// Turn on all optimizations and checks
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseOptimizedControls, true);
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kUseReadValueCaching, true);
InputSystem.settings.SetInternalFeatureFlag(InputFeatureNames.kParanoidReadValueCachingChecks, true);
#if UNITY_EDITOR
// Default mock dialogs to avoid unexpected cancellation of standard flows
Dialog.InputActionAsset.SetSaveChanges((_) => Dialog.Result.Discard);
Dialog.InputActionAsset.SetDiscardUnsavedChanges((_) => Dialog.Result.Discard);
Dialog.InputActionAsset.SetCreateAndOverwriteExistingAsset((_) => Dialog.Result.Discard);
Dialog.ControlScheme.SetDeleteControlScheme((_) => Dialog.Result.Delete);
#endif
}
catch (Exception exception)
{
Debug.LogError("Failed to set up input system for test " + TestContext.CurrentContext.Test.Name);
Debug.LogException(exception);
throw;
}
m_Initialized = true;
if (InputSystem.devices.Count > 0)
Assert.Fail("Input system should not have devices after reset");
}
/// <summary>
/// Restore the state of the input system it had when the test was started.
/// </summary>
/// <seealso cref="Setup"/>
[TearDown]
public virtual void TearDown()
{
if (!m_Initialized)
return;
try
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
InputSystem.Restore();
#endif
runtime.Dispose();
// Unhook from play mode state changes.
#if UNITY_EDITOR
if (m_OnPlayModeStateChange != null)
EditorApplication.playModeStateChanged -= m_OnPlayModeStateChange;
#endif
// Re-enable input debugger.
#if UNITY_EDITOR
InputDebuggerWindow.Enable();
#endif
#if UNITY_EDITOR
// Re-enable dialogs.
Dialog.InputActionAsset.SetSaveChanges(null);
Dialog.InputActionAsset.SetDiscardUnsavedChanges(null);
Dialog.InputActionAsset.SetCreateAndOverwriteExistingAsset(null);
Dialog.ControlScheme.SetDeleteControlScheme(null);
#endif
}
catch (Exception exception)
{
Debug.LogError("Failed to shut down and restore input system after test " + TestContext.CurrentContext.Test.Name);
Debug.LogException(exception);
throw;
}
m_Initialized = false;
}
private bool? m_IsUnityTest;
private Test m_CurrentTest;
// True if the current test is a [UnityTest].
private bool IsUnityTest()
{
// We cache this value so that any call after the first in a test no
// longer allocates GC memory. Otherwise we'll run into trouble with
// DoesNotAllocate tests.
var test = TestContext.CurrentTestExecutionContext.CurrentTest;
if (m_IsUnityTest.HasValue && m_CurrentTest == test)
return m_IsUnityTest.Value;
var className = test.ClassName;
var methodName = test.MethodName;
// Doesn't seem like there's a proper way to get the current test method based on
// the information provided by NUnit (see https://github.com/nunit/nunit/issues/3354).
var type = Type.GetType(className);
if (type == null)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
type = assembly.GetType(className);
if (type != null)
break;
}
}
if (type == null)
{
m_IsUnityTest = false;
}
else
{
var method = type.GetMethod(methodName);
m_IsUnityTest = method?.GetCustomAttribute<UnityTestAttribute>() != null;
}
m_CurrentTest = test;
return m_IsUnityTest.Value;
}
#if UNITY_EDITOR
private Action<PlayModeStateChange> m_OnPlayModeStateChange;
private void OnPlayModeStateChange(PlayModeStateChange change)
{
if (change == PlayModeStateChange.ExitingPlayMode && m_Initialized)
TearDown();
}
#endif
// ReSharper disable once MemberCanBeProtected.Global
public static void AssertButtonPress<TState>(InputDevice device, TState state, params ButtonControl[] buttons)
where TState : struct, IInputStateTypeInfo
{
// Update state.
InputSystem.QueueStateEvent(device, state);
InputSystem.Update();
// Now verify that only the buttons we expect to be pressed are pressed.
foreach (var control in device.allControls)
{
if (!(control is ButtonControl controlAsButton))
continue;
var isInList = buttons.Contains(controlAsButton);
if (!isInList)
Assert.That(controlAsButton.isPressed, Is.False,
$"Expected button {controlAsButton} to NOT be pressed");
else
Assert.That(controlAsButton.isPressed, Is.True,
$"Expected button {controlAsButton} to be pressed");
}
}
public static void AssertStickValues(StickControl stick, Vector2 stickValue, float up, float down, float left,
float right)
{
Assert.That(stick.ReadUnprocessedValue(), Is.EqualTo(stickValue));
Assert.That(stick.up.ReadUnprocessedValue(), Is.EqualTo(up).Within(0.0001), "Incorrect 'up' value");
Assert.That(stick.down.ReadUnprocessedValue(), Is.EqualTo(down).Within(0.0001), "Incorrect 'down' value");
Assert.That(stick.left.ReadUnprocessedValue(), Is.EqualTo(left).Within(0.0001), "Incorrect 'left' value");
Assert.That(stick.right.ReadUnprocessedValue(), Is.EqualTo(right).Within(0.0001), "Incorrect 'right' value");
}
private Dictionary<Key, Tuple<string, int>> m_KeyInfos;
private bool m_Initialized;
/// <summary>
/// Set <see cref="Keyboard.keyboardLayout"/> of the given keyboard.
/// </summary>
/// <param name="name">Name of the keyboard layout to switch to.</param>
/// <param name="keyboard">Keyboard to switch layout on. If <c>null</c>, <see cref="Keyboard.current"/> is used.</param>
/// <exception cref="ArgumentException"><paramref name="keyboard"/> and <see cref="Keyboard.current"/> are both <c>null</c>.</exception>
/// <remarks>
/// Also queues and immediately processes an <see cref="DeviceConfigurationEvent"/> for the keyboard.
/// </remarks>
public unsafe void SetKeyboardLayout(string name, Keyboard keyboard = null)
{
if (keyboard == null)
{
keyboard = Keyboard.current;
if (keyboard == null)
throw new ArgumentException("No keyboard has been created and no keyboard has been given", nameof(keyboard));
}
runtime.SetDeviceCommandCallback(keyboard, (id, command) =>
{
if (id == QueryKeyboardLayoutCommand.Type)
{
var commandPtr = (QueryKeyboardLayoutCommand*)command;
commandPtr->WriteLayoutName(name);
return InputDeviceCommand.GenericSuccess;
}
return InputDeviceCommand.GenericFailure;
});
// Make sure caches on keys are flushed.
InputSystem.QueueConfigChangeEvent(Keyboard.current);
InputSystem.Update();
}
/// <summary>
/// Set the <see cref="InputControl.displayName"/> of <paramref name="key"/> on the current
/// <see cref="Keyboard"/> to be <paramref name="displayName"/>.
/// </summary>
/// <param name="key">Key to set the display name for.</param>
/// <param name="displayName">Display name for the key.</param>
/// <param name="scanCode">Optional <see cref="KeyControl.scanCode"/> to report for the key.</param>
/// <remarks>
/// Automatically adds a <see cref="Keyboard"/> if none has been added yet.
/// </remarks>
public unsafe void SetKeyInfo(Key key, string displayName, int scanCode = 0)
{
if (Keyboard.current == null)
InputSystem.AddDevice<Keyboard>();
if (m_KeyInfos == null)
{
m_KeyInfos = new Dictionary<Key, Tuple<string, int>>();
runtime.SetDeviceCommandCallback(Keyboard.current,
(id, commandPtr) =>
{
if (commandPtr->type == QueryKeyNameCommand.Type)
{
var keyNameCommand = (QueryKeyNameCommand*)commandPtr;
if (m_KeyInfos.TryGetValue((Key)keyNameCommand->scanOrKeyCode, out var info))
{
keyNameCommand->scanOrKeyCode = info.Item2;
StringHelpers.WriteStringToBuffer(info.Item1, (IntPtr)keyNameCommand->nameBuffer,
QueryKeyNameCommand.kMaxNameLength);
}
return QueryKeyNameCommand.kSize;
}
return InputDeviceCommand.GenericFailure;
});
}
m_KeyInfos[key] = new Tuple<string, int>(displayName, scanCode);
// Make sure caches on keys are flushed.
InputSystem.QueueConfigChangeEvent(Keyboard.current);
InputSystem.Update();
}
/// <summary>
/// Add support for <see cref="QueryCanRunInBackground"/> to <paramref name="device"/> and return
/// <paramref name="value"/> as <see cref="QueryCanRunInBackground.canRunInBackground"/>.
/// </summary>
/// <param name="device"></param>
internal unsafe void SetCanRunInBackground(InputDevice device, bool canRunInBackground = true)
{
runtime.SetDeviceCommandCallback(device, (id, command) =>
{
if (command->type == QueryCanRunInBackground.Type)
{
((QueryCanRunInBackground*)command)->canRunInBackground = canRunInBackground;
return InputDeviceCommand.GenericSuccess;
}
return InputDeviceCommand.GenericFailure;
});
}
public ActionConstraint Started(InputAction action, InputControl control = null, double? time = null, object value = null)
{
return new ActionConstraint(InputActionPhase.Started, action, control, time: time, duration: 0, value: value);
}
public ActionConstraint Started<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null)
where TValue : struct
{
return new ActionConstraint(InputActionPhase.Started, action, control, value, time: time, duration: 0);
}
public ActionConstraint Performed(InputAction action, InputControl control = null, double? time = null, double? duration = null, object value = null)
{
return new ActionConstraint(InputActionPhase.Performed, action, control, time: time, duration: duration, value: value);
}
public ActionConstraint Performed<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null, double? duration = null)
where TValue : struct
{
return new ActionConstraint(InputActionPhase.Performed, action, control, value, time: time, duration: duration);
}
public ActionConstraint Canceled(InputAction action, InputControl control = null, double? time = null, double? duration = null, object value = null)
{
return new ActionConstraint(InputActionPhase.Canceled, action, control, time: time, duration: duration, value: value);
}
public ActionConstraint Canceled<TValue>(InputAction action, InputControl<TValue> control, TValue value, double? time = null, double? duration = null)
where TValue : struct
{
return new ActionConstraint(InputActionPhase.Canceled, action, control, value, time: time, duration: duration);
}
public ActionConstraint Started<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null)
where TInteraction : IInputInteraction
{
return new ActionConstraint(InputActionPhase.Started, action, control, interaction: typeof(TInteraction), time: time,
duration: 0, value: value);
}
public ActionConstraint Performed<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null)
where TInteraction : IInputInteraction
{
return new ActionConstraint(InputActionPhase.Performed, action, control, interaction: typeof(TInteraction), time: time,
duration: duration, value: value);
}
public ActionConstraint Canceled<TInteraction>(InputAction action, InputControl control = null, object value = null, double? time = null, double? duration = null)
where TInteraction : IInputInteraction
{
return new ActionConstraint(InputActionPhase.Canceled, action, control, interaction: typeof(TInteraction), time: time,
duration: duration, value: value);
}
////REVIEW: Should we determine queueEventOnly automatically from whether we're in a UnityTest?
// ReSharper disable once MemberCanBeProtected.Global
public void Press(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
{
Set(button, 1, time, timeOffset, queueEventOnly: queueEventOnly);
}
// ReSharper disable once MemberCanBeProtected.Global
public void Release(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
{
Set(button, 0, time, timeOffset, queueEventOnly: queueEventOnly);
}
// ReSharper disable once MemberCanBePrivate.Global
public void PressAndRelease(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
{
Press(button, time, timeOffset, queueEventOnly: true); // This one is always just a queue.
Release(button, time, timeOffset, queueEventOnly: queueEventOnly);
}
// ReSharper disable once MemberCanBeProtected.Global
public void Click(ButtonControl button, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
{
PressAndRelease(button, time, timeOffset, queueEventOnly: queueEventOnly);
}
/// <summary>
/// Set the control with the given <paramref name="path"/> on <paramref name="device"/> to the given <paramref name="state"/>
/// by sending a state event with the value to the device.
/// </summary>
/// <param name="device">Device on which to find a control.</param>
/// <param name="path">Path of the control on the device.</param>
/// <param name="state">New state for the control.</param>
/// <param name="time">Timestamp to use for the state event. If -1 (default), current time is used (see <see cref="InputTestFixture.currentTime"/>).</param>
/// <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>
/// <param name="queueEventOnly">If true, no <see cref="InputSystem.Update"/> will be performed after queueing the event. This will only put
/// 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.
/// 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
/// as they will not yet see the state change.
///
/// Note that this parameter will be ignored if the test is a <c>[UnityTest]</c>. Multi-frame
/// playmode tests will automatically process input as part of the Unity player loop.</param>
/// <typeparam name="TValue">Value type of the control.</typeparam>
/// <example>
/// <code>
/// var device = InputSystem.AddDevice("TestDevice");
/// Set&lt;ButtonControl&gt;(device, "button", 1);
/// Set&lt;AxisControl&gt;(device, "{Primary2DMotion}/x", 123.456f);
/// </code>
/// </example>
public void Set<TValue>(InputDevice device, string path, TValue state, double time = -1, double timeOffset = 0,
bool queueEventOnly = false)
where TValue : struct
{
if (device == null)
throw new ArgumentNullException(nameof(device));
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
var control = (InputControl<TValue>)device[path];
Set(control, state, time, timeOffset, queueEventOnly);
}
/// <summary>
/// Set the control to the given value by sending a state event with the value to the
/// control's device.
/// </summary>
/// <param name="control">An input control on a device that has been added to the system.</param>
/// <param name="state">New value for the input control.</param>
/// <param name="time">Timestamp to use for the state event. If -1 (default), current time is used (see <see cref="InputTestFixture.currentTime"/>).</param>
/// <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>
/// <param name="queueEventOnly">If true, no <see cref="InputSystem.Update"/> will be performed after queueing the event. This will only put
/// 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.
/// 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
/// as they will not yet see the state change.
///
/// Note that this parameter will be ignored if the test is a <c>[UnityTest]</c>. Multi-frame
/// playmode tests will automatically process input as part of the Unity player loop.</param>
/// <typeparam name="TValue">Value type of the given control.</typeparam>
/// <example>
/// <code>
/// var gamepad = InputSystem.AddDevice&lt;Gamepad&gt;();
/// Set(gamepad.leftButton, 1);
/// </code>
/// </example>
public void Set<TValue>(InputControl<TValue> control, TValue state, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
where TValue : struct
{
if (control == null)
throw new ArgumentNullException(nameof(control));
if (!control.device.added)
throw new ArgumentException(
$"Device of control '{control}' has not been added to the system", nameof(control));
if (IsUnityTest())
queueEventOnly = true;
void SetUpAndQueueEvent(InputEventPtr eventPtr)
{
eventPtr.time = (time >= 0 ? time : InputState.currentTime) + timeOffset;
control.WriteValueIntoEvent(state, eventPtr);
InputSystem.QueueEvent(eventPtr);
}
// Touchscreen does not support delta events involving TouchState.
if (control is TouchControl)
{
using (StateEvent.From(control.device, out var eventPtr))
SetUpAndQueueEvent(eventPtr);
}
else
{
// We use delta state events rather than full state events here to mitigate the following problem:
// Grabbing state from the device will preserve the current values of controls covered in the state.
// However, running an update may alter the value of one or more of those controls. So with a full
// state event, we may be writing outdated data back into the device. For example, in the case of delta
// controls which will reset in OnBeforeUpdate().
//
// Using delta events, we may still grab state outside of just the one control in case we're looking at
// bit-addressed controls but at least we can avoid the problem for the majority of controls.
using (DeltaStateEvent.From(control, out var eventPtr))
SetUpAndQueueEvent(eventPtr);
}
if (!queueEventOnly)
InputSystem.Update();
}
public void Move(InputControl<Vector2> positionControl, Vector2 position, Vector2? delta = null, double time = -1, double timeOffset = 0, bool queueEventOnly = false)
{
Set(positionControl, position, time: time, timeOffset: timeOffset, queueEventOnly: true);
var deltaControl = (Vector2Control)positionControl.device.TryGetChildControl("delta");
if (deltaControl != null)
Set(deltaControl, delta ?? position - positionControl.ReadValue(), time: time, timeOffset: timeOffset, queueEventOnly: true);
if (!queueEventOnly)
InputSystem.Update();
}
////TODO: obsolete this one in 2.0 and use pressure=1 default value
public void BeginTouch(int touchId, Vector2 position, bool queueEventOnly = false, Touchscreen screen = null,
double time = -1, double timeOffset = 0, byte displayIndex = 0)
{
SetTouch(touchId, TouchPhase.Began, position, 1, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset, displayIndex: displayIndex);
}
public void BeginTouch(int touchId, Vector2 position, float pressure, bool queueEventOnly = false, Touchscreen screen = null,
double time = -1, double timeOffset = 0)
{
SetTouch(touchId, TouchPhase.Began, position, pressure, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
}
////TODO: obsolete this one in 2.0 and use pressure=1 default value
public void MoveTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
Touchscreen screen = null, double time = -1, double timeOffset = 0)
{
SetTouch(touchId, TouchPhase.Moved, position, 1, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
}
public void MoveTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false,
Touchscreen screen = null, double time = -1, double timeOffset = 0)
{
SetTouch(touchId, TouchPhase.Moved, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
}
////TODO: obsolete this one in 2.0 and use pressure=1 default value
public void EndTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
Touchscreen screen = null, double time = -1, double timeOffset = 0, byte displayIndex = 0)
{
SetTouch(touchId, TouchPhase.Ended, position, 1, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset, displayIndex: displayIndex);
}
public void EndTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false,
Touchscreen screen = null, double time = -1, double timeOffset = 0)
{
SetTouch(touchId, TouchPhase.Ended, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
}
////TODO: obsolete this one in 2.0 and use pressure=1 default value
public void CancelTouch(int touchId, Vector2 position, Vector2 delta = default, bool queueEventOnly = false,
Touchscreen screen = null, double time = -1, double timeOffset = 0)
{
SetTouch(touchId, TouchPhase.Canceled, position, delta, queueEventOnly: queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
}
public void CancelTouch(int touchId, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = false,
Touchscreen screen = null, double time = -1, double timeOffset = 0)
{
SetTouch(touchId, TouchPhase.Canceled, position, pressure, delta, queueEventOnly, screen: screen, time: time, timeOffset: timeOffset);
}
////TODO: obsolete this one in 2.0 and use pressure=1 default value
public void SetTouch(int touchId, TouchPhase phase, Vector2 position, Vector2 delta = default,
bool queueEventOnly = true, Touchscreen screen = null, double time = -1, double timeOffset = 0)
{
SetTouch(touchId, phase, position, 1, delta: delta, queueEventOnly: queueEventOnly, screen: screen, time: time,
timeOffset: timeOffset);
}
public void SetTouch(int touchId, TouchPhase phase, Vector2 position, float pressure, Vector2 delta = default, bool queueEventOnly = true,
Touchscreen screen = null, double time = -1, double timeOffset = 0, byte displayIndex = 0)
{
if (screen == null)
{
screen = Touchscreen.current;
if (screen == null)
screen = InputSystem.AddDevice<Touchscreen>();
}
InputSystem.QueueStateEvent(screen, new TouchState
{
touchId = touchId,
phase = phase,
position = position,
delta = delta,
pressure = pressure,
displayIndex = displayIndex,
}, (time >= 0 ? time : InputState.currentTime) + timeOffset);
if (!queueEventOnly)
InputSystem.Update();
}
public void Trigger<TValue>(InputAction action, InputControl<TValue> control, TValue value)
where TValue : struct
{
throw new NotImplementedException();
}
/// <summary>
/// Perform the input action without having to know what it is bound to.
/// </summary>
/// <param name="action">An input action that is currently enabled and has controls it is bound to.</param>
/// <remarks>
/// Blindly triggering an action requires making a few assumptions. Actions are not built to be able to trigger
/// without any input. This means that this method has to generate input on a control that the action is bound to.
///
/// Note that this method has no understanding of the interactions that may be present on the action and thus
/// does not know how they may affect the triggering of the action.
/// </remarks>
public void Trigger(InputAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (!action.enabled)
throw new ArgumentException(
$"Action '{action}' must be enabled in order to be able to trigger it", nameof(action));
var controls = action.controls;
if (controls.Count == 0)
throw new ArgumentException(
$"Action '{action}' must be bound to controls in order to be able to trigger it", nameof(action));
// See if we have a button we can trigger.
for (var i = 0; i < controls.Count; ++i)
{
if (!(controls[i] is ButtonControl button))
continue;
// Press and release button.
Set(button, 1);
Set(button, 0);
return;
}
// See if we have an axis we can slide a bit.
for (var i = 0; i < controls.Count; ++i)
{
if (!(controls[i] is AxisControl axis))
continue;
// We do, so nudge its value a bit.
Set(axis, axis.ReadValue() + 0.01f);
return;
}
////TODO: support a wider range of controls
throw new NotImplementedException();
}
/// <summary>
/// The input runtime used during testing.
/// </summary>
internal InputTestRuntime runtime { get; private set; }
/// <summary>
/// Get or set the current time used by the input system.
/// </summary>
/// <value>Current time used by the input system.</value>
public double currentTime
{
get => runtime.currentTime - runtime.currentTimeOffsetToRealtimeSinceStartup;
set
{
runtime.currentTime = value + runtime.currentTimeOffsetToRealtimeSinceStartup;
runtime.dontAdvanceTimeNextDynamicUpdate = true;
}
}
internal float unscaledGameTime
{
get => runtime.unscaledGameTime;
set
{
runtime.unscaledGameTime = value;
runtime.dontAdvanceUnscaledGameTimeNextDynamicUpdate = true;
}
}
public class ActionConstraint : Constraint
{
public InputActionPhase phase { get; set; }
public double? time { get; set; }
public double? duration { get; set; }
public InputAction action { get; set; }
public InputControl control { get; set; }
public object value { get; set; }
public Type interaction { get; set; }
private readonly List<ActionConstraint> m_AndThen = new List<ActionConstraint>();
public ActionConstraint(InputActionPhase phase, InputAction action, InputControl control, object value = null, Type interaction = null, double? time = null, double? duration = null)
{
this.phase = phase;
this.time = time;
this.duration = duration;
this.action = action;
this.control = control;
this.value = value;
this.interaction = interaction;
var interactionText = string.Empty;
if (interaction != null)
interactionText = InputInteraction.GetDisplayName(interaction);
var actionName = action.actionMap != null ? $"{action.actionMap}/{action.name}" : action.name;
// Use same text format as InputActionTrace for easier comparison.
var description = $"{{ action={actionName} phase={phase}";
if (time != null)
description += $" time={time}";
if (control != null)
description += $" control={control}";
if (value != null)
description += $" value={value}";
if (interaction != null)
description += $" interaction={interactionText}";
if (duration != null)
description += $" duration={duration}";
description += " }";
Description = description;
}
public override ConstraintResult ApplyTo(object actual)
{
var trace = (InputActionTrace)actual;
var actions = trace.ToArray();
if (actions.Length == 0)
return new ConstraintResult(this, actual, false);
if (!Verify(actions[0]))
return new ConstraintResult(this, actual, false);
var i = 1;
foreach (var constraint in m_AndThen)
{
if (i >= actions.Length || !constraint.Verify(actions[i]))
return new ConstraintResult(this, actual, false);
++i;
}
if (i != actions.Length)
return new ConstraintResult(this, actual, false);
return new ConstraintResult(this, actual, true);
}
private bool Verify(InputActionTrace.ActionEventPtr eventPtr)
{
// NOTE: Using explicit "return false" branches everywhere for easier setting of breakpoints.
if (eventPtr.action != action ||
eventPtr.phase != phase)
return false;
// Check time.
if (time != null && !Mathf.Approximately((float)time.Value, (float)eventPtr.time))
return false;
// Check duration.
if (duration != null && !Mathf.Approximately((float)duration.Value, (float)eventPtr.duration))
return false;
// Check control.
if (control != null && eventPtr.control != control)
return false;
// Check interaction.
if (interaction != null && (eventPtr.interaction == null ||
!interaction.IsInstanceOfType(eventPtr.interaction)))
return false;
// Check value.
if (value != null)
{
var val = eventPtr.ReadValueAsObject();
if (val is float f)
{
if (!Mathf.Approximately(f, Convert.ToSingle(value)))
return false;
}
else if (val is double d)
{
if (!Mathf.Approximately((float)d, (float)Convert.ToDouble(value)))
return false;
}
else if (val is Vector2 v2)
{
if (!Vector2EqualityComparer.Instance.Equals(v2, value.As<Vector2>()))
return false;
}
else if (val is Vector3 v3)
{
if (!Vector3EqualityComparer.Instance.Equals(v3, value.As<Vector3>()))
return false;
}
else if (!val.Equals(value))
return false;
}
return true;
}
public ActionConstraint AndThen(ActionConstraint constraint)
{
m_AndThen.Add(constraint);
Description += " and\n";
Description += constraint.Description;
return this;
}
}
#if UNITY_EDITOR
internal void SimulateDomainReload()
{
// This quite invasively goes into InputSystem internals. Unfortunately, we
// have no proper way of simulating domain reloads ATM. So we directly call various
// internal methods here in a sequence similar to what we'd get during a domain reload.
InputSystem.s_SystemObject.OnBeforeSerialize();
InputSystem.s_SystemObject = null;
InputSystem.InitializeInEditor(runtime);
}
#endif
#if UNITY_EDITOR
/// <summary>
/// Represents an analytics registration event captured by test harness.
/// </summary>
protected struct AnalyticsRegistrationEventData
{
public AnalyticsRegistrationEventData(string name, int maxPerHour, int maxPropertiesPerEvent)
{
this.name = name;
this.maxPerHour = maxPerHour;
this.maxPropertiesPerEvent = maxPropertiesPerEvent;
}
public readonly string name;
public readonly int maxPerHour;
public readonly int maxPropertiesPerEvent;
}
/// <summary>
/// Represents an analytics data event captured by test harness.
/// </summary>
protected struct AnalyticsEventData
{
public AnalyticsEventData(string name, object data)
{
this.name = name;
this.data = data;
}
public readonly string name;
public readonly object data;
}
private List<AnalyticsRegistrationEventData> m_RegisteredAnalytics;
private List<AnalyticsEventData> m_SentAnalyticsEvents;
/// <summary>
/// Returns a read-only list of all analytics events registred by enabling capture via <see cref="CollectAnalytics(System.Predicate{string})"/>.
/// </summary>
protected IReadOnlyList<AnalyticsRegistrationEventData> registeredAnalytics => m_RegisteredAnalytics;
/// <summary>
/// Returns a read-only list of all analytics events captured by enabling capture via <see cref="CollectAnalytics(System.Predicate{string})"/>.
/// </summary>
protected IReadOnlyList<AnalyticsEventData> sentAnalyticsEvents => m_SentAnalyticsEvents;
/// <summary>
/// Set up the test fixture to collect analytics registrations and events
/// </summary>
/// <param name="analyticsNameFilter">A filter predicate evaluating whether the given analytics name should be accepted to be stored in test fixture.</param>
protected void CollectAnalytics(Predicate<string> analyticsNameFilter)
{
// Make sure containers are initialized and create them if not. Otherwise just clear to avoid allocation.
if (m_RegisteredAnalytics == null)
m_RegisteredAnalytics = new List<AnalyticsRegistrationEventData>();
else
m_RegisteredAnalytics.Clear();
if (m_SentAnalyticsEvents == null)
m_SentAnalyticsEvents = new List<AnalyticsEventData>();
else
m_SentAnalyticsEvents.Clear();
// Store registered analytics when called if filter applies
runtime.onRegisterAnalyticsEvent = (name, maxPerHour, maxPropertiesPerEvent) =>
{
if (analyticsNameFilter(name))
m_RegisteredAnalytics.Add(new AnalyticsRegistrationEventData(name: name, maxPerHour: maxPerHour, maxPropertiesPerEvent: maxPropertiesPerEvent));
};
// Store sent analytic events when called if filter applies
runtime.onSendAnalyticsEvent = (name, data) =>
{
if (analyticsNameFilter(name))
m_SentAnalyticsEvents.Add(new AnalyticsEventData(name: name, data: data));
};
}
/// <summary>
/// Set up the test fixture to collect filtered analytics registrations and events.
/// </summary>
/// <param name="acceptedName">The analytics name to be accepted, all other registrations and data
/// will be discarded.</param>
protected void CollectAnalytics(string acceptedName)
{
CollectAnalytics((name) => name.Equals(acceptedName));
}
/// <summary>
/// Set up the test fixture to collect ALL analytics registrations and events.
/// </summary>
protected void CollectAnalytics()
{
CollectAnalytics((_) => true);
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81128cc364d234170845119461c41970
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,486 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine.InputSystem.LowLevel;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.Analytics;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace UnityEngine.InputSystem
{
/// <summary>
/// An implementation of <see cref="IInputRuntime"/> for use during tests.
/// </summary>
/// <remarks>
/// This class is only available in the editor and in development players.
///
/// The test runtime replaces the services usually supplied by <see cref="UnityEngineInternal.Input.NativeInputSystem"/>.
/// </remarks>
/// <seealso cref="InputTestFixture.runtime"/>
internal class InputTestRuntime : IInputRuntime, IDisposable
{
public unsafe delegate long DeviceCommandCallback(int deviceId, InputDeviceCommand* command);
~InputTestRuntime()
{
Dispose();
}
public int AllocateDeviceId()
{
var result = m_NextDeviceId;
++m_NextDeviceId;
return result;
}
public unsafe void Update(InputUpdateType type)
{
if (!onShouldRunUpdate.Invoke(type))
return;
lock (m_Lock)
{
if (type == InputUpdateType.Dynamic && !dontAdvanceUnscaledGameTimeNextDynamicUpdate)
{
unscaledGameTime += 1 / 30f;
dontAdvanceUnscaledGameTimeNextDynamicUpdate = false;
}
if (m_NewDeviceDiscoveries != null && m_NewDeviceDiscoveries.Count > 0)
{
if (onDeviceDiscovered != null)
foreach (var entry in m_NewDeviceDiscoveries)
onDeviceDiscovered(entry.Key, entry.Value);
m_NewDeviceDiscoveries.Clear();
}
onBeforeUpdate?.Invoke(type);
// Advance time *after* onBeforeUpdate so that events generated from onBeforeUpdate
// don't get bumped into the following update.
if (type == InputUpdateType.Dynamic && !dontAdvanceTimeNextDynamicUpdate)
{
currentTime += advanceTimeEachDynamicUpdate;
dontAdvanceTimeNextDynamicUpdate = false;
}
if (onUpdate != null)
{
var buffer = new InputEventBuffer(
(InputEvent*)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(m_EventBuffer),
m_EventCount, m_EventWritePosition, m_EventBuffer.Length);
try
{
onUpdate(type, ref buffer);
}
catch (Exception e)
{
// Same order as in NativeInputRuntime
Debug.LogException(e);
Debug.LogError($"{e.GetType().Name} during event processing of {type} update; resetting event buffer");
// Rethrow exception for test runtime to enable us to assert against it in tests.
m_EventCount = 0;
m_EventWritePosition = 0;
throw;
}
m_EventCount = buffer.eventCount;
m_EventWritePosition = (int)buffer.sizeInBytes;
if (NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(buffer.data) !=
NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(m_EventBuffer))
m_EventBuffer = buffer.data;
}
else
{
m_EventCount = 0;
m_EventWritePosition = 0;
}
}
}
public unsafe void QueueEvent(InputEvent* eventPtr)
{
var eventSize = eventPtr->sizeInBytes;
var alignedEventSize = eventSize.AlignToMultipleOf(4);
lock (m_Lock)
{
eventPtr->eventId = m_NextEventId;
eventPtr->handled = false;
++m_NextEventId;
// Enlarge buffer, if we have to.
if ((m_EventWritePosition + alignedEventSize) > m_EventBuffer.Length)
{
var newBufferSize = m_EventBuffer.Length + Mathf.Max((int)alignedEventSize, 1024);
var newBuffer = new NativeArray<byte>(newBufferSize, Allocator.Persistent);
UnsafeUtility.MemCpy(newBuffer.GetUnsafePtr(), m_EventBuffer.GetUnsafePtr(), m_EventWritePosition);
m_EventBuffer.Dispose();
m_EventBuffer = newBuffer;
}
// Copy event.
UnsafeUtility.MemCpy((byte*)m_EventBuffer.GetUnsafePtr() + m_EventWritePosition, eventPtr, eventSize);
m_EventWritePosition += (int)alignedEventSize;
++m_EventCount;
}
}
public unsafe void SetCanRunInBackground(int deviceId)
{
SetDeviceCommandCallback(deviceId,
(id, command) =>
{
if (command->type == QueryCanRunInBackground.Type)
{
((QueryCanRunInBackground*)command)->canRunInBackground = true;
return InputDeviceCommand.GenericSuccess;
}
return InputDeviceCommand.GenericFailure;
});
}
public void SetDeviceCommandCallback(InputDevice device, DeviceCommandCallback callback)
{
SetDeviceCommandCallback(device.deviceId, callback);
}
public void SetDeviceCommandCallback(int deviceId, DeviceCommandCallback callback)
{
lock (m_Lock)
{
if (m_DeviceCommandCallbacks == null)
m_DeviceCommandCallbacks = new List<KeyValuePair<int, DeviceCommandCallback>>();
else
{
for (var i = 0; i < m_DeviceCommandCallbacks.Count; ++i)
{
if (m_DeviceCommandCallbacks[i].Key == deviceId)
{
m_DeviceCommandCallbacks[i] = new KeyValuePair<int, DeviceCommandCallback>(deviceId, callback);
return;
}
}
}
m_DeviceCommandCallbacks.Add(new KeyValuePair<int, DeviceCommandCallback>(deviceId, callback));
}
}
public void SetDeviceCommandCallback<TCommand>(int deviceId, TCommand result)
where TCommand : struct, IInputDeviceCommandInfo
{
bool? receivedCommand = null;
unsafe
{
SetDeviceCommandCallback(deviceId,
(id, commandPtr) =>
{
if (commandPtr->type == result.typeStatic)
{
Assert.That(receivedCommand.HasValue, Is.False);
receivedCommand = true;
UnsafeUtility.MemCpy(commandPtr, UnsafeUtility.AddressOf(ref result),
UnsafeUtility.SizeOf<TCommand>());
return InputDeviceCommand.GenericSuccess;
}
return InputDeviceCommand.GenericFailure;
});
}
}
public unsafe long DeviceCommand(int deviceId, InputDeviceCommand* commandPtr)
{
lock (m_Lock)
{
if (commandPtr->type == QueryPairedUserAccountCommand.Type)
{
foreach (var pairing in userAccountPairings)
{
if (pairing.deviceId != deviceId)
continue;
var queryPairedUser = (QueryPairedUserAccountCommand*)commandPtr;
queryPairedUser->handle = pairing.userHandle;
queryPairedUser->name = pairing.userName;
queryPairedUser->id = pairing.userId;
return (long)QueryPairedUserAccountCommand.Result.DevicePairedToUserAccount;
}
}
var result = InputDeviceCommand.GenericFailure;
if (m_DeviceCommandCallbacks != null)
foreach (var entry in m_DeviceCommandCallbacks)
{
if (entry.Key == deviceId)
{
result = entry.Value(deviceId, commandPtr);
if (result >= 0)
return result;
}
}
return result;
}
}
public void InvokePlayerFocusChanged(bool newFocusState)
{
m_HasFocus = newFocusState;
onPlayerFocusChanged?.Invoke(newFocusState);
}
public void PlayerFocusLost()
{
InvokePlayerFocusChanged(false);
}
public void PlayerFocusGained()
{
InvokePlayerFocusChanged(true);
}
public int ReportNewInputDevice(string deviceDescriptor, int deviceId = InputDevice.InvalidDeviceId)
{
lock (m_Lock)
{
if (deviceId == InputDevice.InvalidDeviceId)
deviceId = AllocateDeviceId();
if (m_NewDeviceDiscoveries == null)
m_NewDeviceDiscoveries = new List<KeyValuePair<int, string>>();
m_NewDeviceDiscoveries.Add(new KeyValuePair<int, string>(deviceId, deviceDescriptor));
return deviceId;
}
}
public int ReportNewInputDevice(InputDeviceDescription description, int deviceId = InputDevice.InvalidDeviceId,
ulong userHandle = 0, string userName = null, string userId = null)
{
deviceId = ReportNewInputDevice(description.ToJson(), deviceId);
// If we have user information, automatically set up
if (userHandle != 0)
AssociateInputDeviceWithUser(deviceId, userHandle, userName, userId);
return deviceId;
}
public int ReportNewInputDevice<TDevice>(int deviceId = InputDevice.InvalidDeviceId,
ulong userHandle = 0, string userName = null, string userId = null)
where TDevice : InputDevice
{
return ReportNewInputDevice(
new InputDeviceDescription {deviceClass = typeof(TDevice).Name, interfaceName = "Test"}, deviceId,
userHandle, userName, userId);
}
public unsafe void ReportInputDeviceRemoved(int deviceId)
{
var removeEvent = DeviceRemoveEvent.Create(deviceId);
var removeEventPtr = UnsafeUtility.AddressOf(ref removeEvent);
QueueEvent((InputEvent*)removeEventPtr);
}
public void ReportInputDeviceRemoved(InputDevice device)
{
if (device == null)
throw new ArgumentNullException(nameof(device));
ReportInputDeviceRemoved(device.deviceId);
}
public void AssociateInputDeviceWithUser(int deviceId, ulong userHandle, string userName = null, string userId = null)
{
var existingIndex = -1;
for (var i = 0; i < userAccountPairings.Count; ++i)
if (userAccountPairings[i].deviceId == deviceId)
{
existingIndex = i;
break;
}
if (userHandle == 0)
{
if (existingIndex != -1)
userAccountPairings.RemoveAt(existingIndex);
}
else if (existingIndex != -1)
{
userAccountPairings[existingIndex] =
new PairedUser
{
deviceId = deviceId,
userHandle = userHandle,
userName = userName,
userId = userId,
};
}
else
{
userAccountPairings.Add(
new PairedUser
{
deviceId = deviceId,
userHandle = userHandle,
userName = userName,
userId = userId,
});
}
}
public void AssociateInputDeviceWithUser(InputDevice device, ulong userHandle, string userName = null, string userId = null)
{
AssociateInputDeviceWithUser(device.deviceId, userHandle, userName, userId);
}
public struct PairedUser
{
public int deviceId;
public ulong userHandle;
public string userName;
public string userId;
}
public InputUpdateDelegate onUpdate { get; set; }
public Action<InputUpdateType> onBeforeUpdate { get; set; }
public Func<InputUpdateType, bool> onShouldRunUpdate { get; set; }
#if UNITY_EDITOR
public Action onPlayerLoopInitialization { get; set; }
#endif
public Action<int, string> onDeviceDiscovered { get; set; }
public Action onShutdown { get; set; }
public Action<bool> onPlayerFocusChanged { get; set; }
public bool isPlayerFocused => m_HasFocus;
public float pollingFrequency { get; set; }
public double currentTime { get; set; }
public double currentTimeForFixedUpdate { get; set; }
public float unscaledGameTime { get; set; } = 1;
public bool dontAdvanceUnscaledGameTimeNextDynamicUpdate { get; set; }
public double advanceTimeEachDynamicUpdate { get; set; } = 1.0 / 60;
public bool dontAdvanceTimeNextDynamicUpdate { get; set; }
public bool runInBackground { get; set; } = false;
public Vector2 screenSize { get; set; } = new Vector2(1024, 768);
public ScreenOrientation screenOrientation { set; get; } = ScreenOrientation.Portrait;
public bool normalizeScrollWheelDelta { get; set; } = true;
public float scrollWheelDeltaPerTick { get; set; } = 1.0f;
public List<PairedUser> userAccountPairings
{
get
{
if (m_UserPairings == null)
m_UserPairings = new List<PairedUser>();
return m_UserPairings;
}
}
public void Dispose()
{
m_EventBuffer.Dispose();
GC.SuppressFinalize(this);
}
public double currentTimeOffsetToRealtimeSinceStartup
{
get => m_CurrentTimeOffsetToRealtimeSinceStartup;
set
{
m_CurrentTimeOffsetToRealtimeSinceStartup = value;
InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup = value;
}
}
#if UNITY_EDITOR
public bool isInPlayMode { get; set; } = true;
public bool isEditorActive { get; set; } = true;
public Func<IntPtr, bool> onUnityRemoteMessage
{
get => m_UnityRemoteMessageHandler;
set => m_UnityRemoteMessageHandler = value;
}
public bool? unityRemoteGyroEnabled;
public float? unityRemoteGyroUpdateInterval;
public void SetUnityRemoteGyroEnabled(bool value)
{
unityRemoteGyroEnabled = value;
}
public void SetUnityRemoteGyroUpdateInterval(float interval)
{
unityRemoteGyroUpdateInterval = interval;
}
public Action<PlayModeStateChange> onPlayModeChanged { get; set; }
public Action onProjectChange { get; set; }
#endif
public int eventCount => m_EventCount;
internal const int kDefaultEventBufferSize = 1024 * 512;
private bool m_HasFocus = true;
private int m_NextDeviceId = 1;
private int m_NextEventId = 1;
internal int m_EventCount;
private int m_EventWritePosition;
private NativeArray<byte> m_EventBuffer = new NativeArray<byte>(kDefaultEventBufferSize, Allocator.Persistent);
private List<PairedUser> m_UserPairings;
private List<KeyValuePair<int, string>> m_NewDeviceDiscoveries;
private List<KeyValuePair<int, DeviceCommandCallback>> m_DeviceCommandCallbacks;
private object m_Lock = new object();
private double m_CurrentTimeOffsetToRealtimeSinceStartup;
private Func<IntPtr, bool> m_UnityRemoteMessageHandler;
#if UNITY_ANALYTICS || UNITY_EDITOR
public Action<string, int, int> onRegisterAnalyticsEvent { get; set; }
public Action<string, object> onSendAnalyticsEvent { get; set; }
public void SendAnalytic(InputAnalytics.IInputAnalytic analytic)
{
#if UNITY_2023_2_OR_NEWER
// Mimic editor analytics for Unity 2023.2+ invoking TryGatherData to send
var analyticInfoAttribute = analytic.GetType().GetCustomAttributes(
typeof(AnalyticInfoAttribute), true).FirstOrDefault() as AnalyticInfoAttribute;
var info = analytic.info;
#if UNITY_EDITOR
// Registration handled by framework
#else
onRegisterAnalyticsEvent?.Invoke(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements); // only to avoid writing two tests per Unity version (registration handled by framework)
#endif
if (analytic.TryGatherData(out var data, out var ex) && data != null && analyticInfoAttribute != null)
onSendAnalyticsEvent?.Invoke(analyticInfoAttribute.eventName, data);
else if (ex != null)
throw ex; // rethrow for visibility in test scope
#else
var info = analytic.info;
onRegisterAnalyticsEvent?.Invoke(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements);
if (analytic.TryGatherData(out var data, out var error))
onSendAnalyticsEvent?.Invoke(info.Name, data);
else
throw error; // For visibility in tests
#endif // UNITY_2023_2_OR_NEWER
}
#endif // UNITY_ANALYTICS || UNITY_EDITOR
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 073f4378d59364199ae19d0486842726
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,57 @@
using System;
using UnityEditor;
namespace UnityEngine.InputSystem
{
// Utility allowing access to object T as well as a dispose delegate to clean-up any resources associated with it.
// Useful with the 'using' keyword to provide scoped RAII-like cleanup of objects in tests without having a
// dedicated fixture handling the clean-up.
internal sealed class ScopedDisposable<T> : IDisposable
where T : UnityEngine.Object
{
private Action<T> m_Dispose;
public ScopedDisposable(T obj, Action<T> dispose)
{
value = obj;
m_Dispose = dispose;
}
public T value
{
get;
private set;
}
public void Dispose()
{
if (m_Dispose == null)
return;
if (value != null)
m_Dispose(value);
m_Dispose = null;
value = null;
}
}
// Convenience API for scoped objects.
internal static class Scoped
{
public static ScopedDisposable<T> Object<T>(T obj) where T : UnityEngine.Object
{
#if UNITY_EDITOR
return new ScopedDisposable<T>(obj, UnityEngine.Object.DestroyImmediate);
#else
return new ScopedDisposable<T>(obj, UnityEngine.Object.Destroy);
#endif
}
#if UNITY_EDITOR
public static ScopedDisposable<T> Asset<T>(T obj) where T : UnityEngine.Object
{
return new ScopedDisposable<T>(obj, (o) => AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(o)));
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5cd4530e3c9047dea0f1cc17dccbf6ec
timeCreated: 1708678710

View File

@@ -0,0 +1,25 @@
{
"name": "Unity.InputSystem.TestFramework",
"references": [
"Unity.InputSystem",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": ["UNITY_TESTS_FRAMEWORK"],
"versionDefines": [
{
"name": "com.unity.test-framework",
"expression": "",
"define": "UNITY_TESTS_FRAMEWORK"
}
],
"versionDefines": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: dc04f38471c3a459fb4d31124ee9127d
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,492 @@
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
using System;
using System.Runtime.InteropServices;
/// <summary>
/// Used to send fake input events to Windows via user32.dll.
/// </summary>
internal static class WinUserInput
{
[DllImport("user32.dll")]
public static extern uint SendInput(
uint nInputs,
[MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs,
int cbSize);
[StructLayout(LayoutKind.Sequential)]
public struct INPUT
{
public InputType type;
public InputUnion U;
public static int Size
{
get { return Marshal.SizeOf(typeof(INPUT)); }
}
}
[StructLayout(LayoutKind.Explicit)]
public struct InputUnion
{
[FieldOffset(0)]
public MOUSEINPUT mi;
[FieldOffset(0)]
public KEYBDINPUT ki;
[FieldOffset(0)]
public HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT
{
public int dx;
public int dy;
public MouseEventDataXButtons mouseData;
public MOUSEEVENTF dwFlags;
public uint time;
public UIntPtr dwExtraInfo;
}
public enum InputType : uint
{
INPUT_MOUSE,
INPUT_KEYBOARD,
INPUT_HARDWARE
}
[Flags]
public enum MouseEventDataXButtons : uint
{
Nothing = 0x00000000,
XBUTTON1 = 0x00000001,
XBUTTON2 = 0x00000002
}
[Flags]
public enum MOUSEEVENTF : uint
{
ABSOLUTE = 0x8000,
HWHEEL = 0x01000,
MOVE = 0x0001,
MOVE_NOCOALESCE = 0x2000,
LEFTDOWN = 0x0002,
LEFTUP = 0x0004,
RIGHTDOWN = 0x0008,
RIGHTUP = 0x0010,
MIDDLEDOWN = 0x0020,
MIDDLEUP = 0x0040,
VIRTUALDESK = 0x4000,
WHEEL = 0x0800,
XDOWN = 0x0080,
XUP = 0x0100
}
[StructLayout(LayoutKind.Sequential)]
public struct KEYBDINPUT
{
public VirtualKeyShort wVk;
public ScanCodeShort wScan;
public KEYEVENTF dwFlags;
public int time;
public UIntPtr dwExtraInfo;
}
[Flags]
public enum KEYEVENTF : uint
{
EXTENDEDKEY = 0x0001,
KEYUP = 0x0002,
SCANCODE = 0x0008,
UNICODE = 0x0004
}
public enum VirtualKeyShort : short
{
LBUTTON = 0x01,
RBUTTON = 0x02,
CANCEL = 0x03,
MBUTTON = 0x04,
XBUTTON1 = 0x05,
XBUTTON2 = 0x06,
BACK = 0x08,
TAB = 0x09,
CLEAR = 0x0C,
RETURN = 0x0D,
SHIFT = 0x10,
CONTROL = 0x11,
MENU = 0x12,
PAUSE = 0x13,
CAPITAL = 0x14,
KANA = 0x15,
HANGUL = 0x15,
JUNJA = 0x17,
FINAL = 0x18,
HANJA = 0x19,
KANJI = 0x19,
ESCAPE = 0x1B,
CONVERT = 0x1C,
NONCONVERT = 0x1D,
ACCEPT = 0x1E,
MODECHANGE = 0x1F,
SPACE = 0x20,
PRIOR = 0x21,
NEXT = 0x22,
END = 0x23,
HOME = 0x24,
LEFT = 0x25,
UP = 0x26,
RIGHT = 0x27,
DOWN = 0x28,
SELECT = 0x29,
PRINT = 0x2A,
EXECUTE = 0x2B,
SNAPSHOT = 0x2C,
INSERT = 0x2D,
DELETE = 0x2E,
HELP = 0x2F,
KEY_0 = 0x30,
KEY_1 = 0x31,
KEY_2 = 0x32,
KEY_3 = 0x33,
KEY_4 = 0x34,
KEY_5 = 0x35,
KEY_6 = 0x36,
KEY_7 = 0x37,
KEY_8 = 0x38,
KEY_9 = 0x39,
KEY_A = 0x41,
KEY_B = 0x42,
KEY_C = 0x43,
KEY_D = 0x44,
KEY_E = 0x45,
KEY_F = 0x46,
KEY_G = 0x47,
KEY_H = 0x48,
KEY_I = 0x49,
KEY_J = 0x4A,
KEY_K = 0x4B,
KEY_L = 0x4C,
KEY_M = 0x4D,
KEY_N = 0x4E,
KEY_O = 0x4F,
KEY_P = 0x50,
KEY_Q = 0x51,
KEY_R = 0x52,
KEY_S = 0x53,
KEY_T = 0x54,
KEY_U = 0x55,
KEY_V = 0x56,
KEY_W = 0x57,
KEY_X = 0x58,
KEY_Y = 0x59,
KEY_Z = 0x5A,
LWIN = 0x5B,
RWIN = 0x5C,
APPS = 0x5D,
SLEEP = 0x5F,
NUMPAD0 = 0x60,
NUMPAD1 = 0x61,
NUMPAD2 = 0x62,
NUMPAD3 = 0x63,
NUMPAD4 = 0x64,
NUMPAD5 = 0x65,
NUMPAD6 = 0x66,
NUMPAD7 = 0x67,
NUMPAD8 = 0x68,
NUMPAD9 = 0x69,
MULTIPLY = 0x6A,
ADD = 0x6B,
SEPARATOR = 0x6C,
SUBTRACT = 0x6D,
DECIMAL = 0x6E,
DIVIDE = 0x6F,
F1 = 0x70,
F2 = 0x71,
F3 = 0x72,
F4 = 0x73,
F5 = 0x74,
F6 = 0x75,
F7 = 0x76,
F8 = 0x77,
F9 = 0x78,
F10 = 0x79,
F11 = 0x7A,
F12 = 0x7B,
F13 = 0x7C,
F14 = 0x7D,
F15 = 0x7E,
F16 = 0x7F,
F17 = 0x80,
F18 = 0x81,
F19 = 0x82,
F20 = 0x83,
F21 = 0x84,
F22 = 0x85,
F23 = 0x86,
F24 = 0x87,
NUMLOCK = 0x90,
SCROLL = 0x91,
LSHIFT = 0xA0,
RSHIFT = 0xA1,
LCONTROL = 0xA2,
RCONTROL = 0xA3,
LMENU = 0xA4,
RMENU = 0xA5,
BROWSER_BACK = 0xA6,
BROWSER_FORWARD = 0xA7,
BROWSER_REFRESH = 0xA8,
BROWSER_STOP = 0xA9,
BROWSER_SEARCH = 0xAA,
BROWSER_FAVORITES = 0xAB,
BROWSER_HOME = 0xAC,
VOLUME_MUTE = 0xAD,
VOLUME_DOWN = 0xAE,
VOLUME_UP = 0xAF,
MEDIA_NEXT_TRACK = 0xB0,
MEDIA_PREV_TRACK = 0xB1,
MEDIA_STOP = 0xB2,
MEDIA_PLAY_PAUSE = 0xB3,
LAUNCH_MAIL = 0xB4,
LAUNCH_MEDIA_SELECT = 0xB5,
LAUNCH_APP1 = 0xB6,
LAUNCH_APP2 = 0xB7,
OEM_1 = 0xBA,
OEM_PLUS = 0xBB,
OEM_COMMA = 0xBC,
OEM_MINUS = 0xBD,
OEM_PERIOD = 0xBE,
OEM_2 = 0xBF,
OEM_3 = 0xC0,
OEM_4 = 0xDB,
OEM_5 = 0xDC,
OEM_6 = 0xDD,
OEM_7 = 0xDE,
OEM_8 = 0xDF,
OEM_102 = 0xE2,
PROCESSKEY = 0xE5,
PACKET = 0xE7,
ATTN = 0xF6,
CRSEL = 0xF7,
EXSEL = 0xF8,
EREOF = 0xF9,
PLAY = 0xFA,
ZOOM = 0xFB,
NONAME = 0xFC,
PA1 = 0xFD,
OEM_CLEAR = 0xFE
}
public enum ScanCodeShort : short
{
LBUTTON = 0,
RBUTTON = 0,
CANCEL = 70,
MBUTTON = 0,
XBUTTON1 = 0,
XBUTTON2 = 0,
BACK = 14,
TAB = 15,
CLEAR = 76,
RETURN = 28,
SHIFT = 42,
CONTROL = 29,
MENU = 56,
PAUSE = 0,
CAPITAL = 58,
KANA = 0,
HANGUL = 0,
JUNJA = 0,
FINAL = 0,
HANJA = 0,
KANJI = 0,
ESCAPE = 1,
CONVERT = 0,
NONCONVERT = 0,
ACCEPT = 0,
MODECHANGE = 0,
SPACE = 57,
PRIOR = 73,
NEXT = 81,
END = 79,
HOME = 71,
LEFT = 75,
UP = 72,
RIGHT = 77,
DOWN = 80,
SELECT = 0,
PRINT = 0,
EXECUTE = 0,
SNAPSHOT = 84,
INSERT = 82,
DELETE = 83,
HELP = 99,
KEY_0 = 11,
KEY_1 = 2,
KEY_2 = 3,
KEY_3 = 4,
KEY_4 = 5,
KEY_5 = 6,
KEY_6 = 7,
KEY_7 = 8,
KEY_8 = 9,
KEY_9 = 10,
KEY_A = 30,
KEY_B = 48,
KEY_C = 46,
KEY_D = 32,
KEY_E = 18,
KEY_F = 33,
KEY_G = 34,
KEY_H = 35,
KEY_I = 23,
KEY_J = 36,
KEY_K = 37,
KEY_L = 38,
KEY_M = 50,
KEY_N = 49,
KEY_O = 24,
KEY_P = 25,
KEY_Q = 16,
KEY_R = 19,
KEY_S = 31,
KEY_T = 20,
KEY_U = 22,
KEY_V = 47,
KEY_W = 17,
KEY_X = 45,
KEY_Y = 21,
KEY_Z = 44,
LWIN = 91,
RWIN = 92,
APPS = 93,
SLEEP = 95,
NUMPAD0 = 82,
NUMPAD1 = 79,
NUMPAD2 = 80,
NUMPAD3 = 81,
NUMPAD4 = 75,
NUMPAD5 = 76,
NUMPAD6 = 77,
NUMPAD7 = 71,
NUMPAD8 = 72,
NUMPAD9 = 73,
MULTIPLY = 55,
ADD = 78,
SEPARATOR = 0,
SUBTRACT = 74,
DECIMAL = 83,
DIVIDE = 53,
F1 = 59,
F2 = 60,
F3 = 61,
F4 = 62,
F5 = 63,
F6 = 64,
F7 = 65,
F8 = 66,
F9 = 67,
F10 = 68,
F11 = 87,
F12 = 88,
F13 = 100,
F14 = 101,
F15 = 102,
F16 = 103,
F17 = 104,
F18 = 105,
F19 = 106,
F20 = 107,
F21 = 108,
F22 = 109,
F23 = 110,
F24 = 118,
NUMLOCK = 69,
SCROLL = 70,
LSHIFT = 42,
RSHIFT = 54,
LCONTROL = 29,
RCONTROL = 29,
LMENU = 56,
RMENU = 56,
BROWSER_BACK = 106,
BROWSER_FORWARD = 105,
BROWSER_REFRESH = 103,
BROWSER_STOP = 104,
BROWSER_SEARCH = 101,
BROWSER_FAVORITES = 102,
BROWSER_HOME = 50,
VOLUME_MUTE = 32,
VOLUME_DOWN = 46,
VOLUME_UP = 48,
MEDIA_NEXT_TRACK = 25,
MEDIA_PREV_TRACK = 16,
MEDIA_STOP = 36,
MEDIA_PLAY_PAUSE = 34,
LAUNCH_MAIL = 108,
LAUNCH_MEDIA_SELECT = 109,
LAUNCH_APP1 = 107,
LAUNCH_APP2 = 33,
OEM_1 = 39,
OEM_PLUS = 13,
OEM_COMMA = 51,
OEM_MINUS = 12,
OEM_PERIOD = 52,
OEM_2 = 53,
OEM_3 = 41,
OEM_4 = 26,
OEM_5 = 43,
OEM_6 = 27,
OEM_7 = 40,
OEM_8 = 0,
OEM_102 = 86,
PROCESSKEY = 0,
PACKET = 0,
ATTN = 0,
CRSEL = 0,
EXSEL = 0,
EREOF = 93,
PLAY = 0,
ZOOM = 98,
NONAME = 0,
PA1 = 0,
OEM_CLEAR = 0,
}
[StructLayout(LayoutKind.Sequential)]
public struct HARDWAREINPUT
{
public int uMsg;
public short wParamL;
public short wParamH;
}
/// <summary>
/// Emulate an event sent over RDP.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
public static void SendRDPMouseMoveEvent(int x, int y)
{
var mouseInput = new INPUT { type = InputType.INPUT_MOUSE };
mouseInput.U.mi.dx = x;
mouseInput.U.mi.dy = y;
mouseInput.U.mi.dwFlags = MOUSEEVENTF.ABSOLUTE | MOUSEEVENTF.VIRTUALDESK | MOUSEEVENTF.MOVE;
SendInput(1, new INPUT[] { mouseInput }, INPUT.Size);
}
/// <summary>
/// Emulate a normal mouse move event with relative coordinates.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
public static void SendMouseMoveEvent(int x, int y)
{
var mouseInput = new INPUT { type = InputType.INPUT_MOUSE };
mouseInput.U.mi.dx = x;
mouseInput.U.mi.dy = y;
mouseInput.U.mi.dwFlags = MOUSEEVENTF.MOVE;
SendInput(1, new INPUT[] { mouseInput }, INPUT.Size);
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e2c8138bd42a9a041bb4b1398ae819d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: