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 { /// /// An implementation of for use during tests. /// /// /// This class is only available in the editor and in development players. /// /// The test runtime replaces the services usually supplied by . /// /// 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(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>(); else { for (var i = 0; i < m_DeviceCommandCallbacks.Count; ++i) { if (m_DeviceCommandCallbacks[i].Key == deviceId) { m_DeviceCommandCallbacks[i] = new KeyValuePair(deviceId, callback); return; } } } m_DeviceCommandCallbacks.Add(new KeyValuePair(deviceId, callback)); } } public void SetDeviceCommandCallback(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()); 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>(); m_NewDeviceDiscoveries.Add(new KeyValuePair(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(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 onBeforeUpdate { get; set; } public Func onShouldRunUpdate { get; set; } #if UNITY_EDITOR public Action onPlayerLoopInitialization { get; set; } #endif public Action onDeviceDiscovered { get; set; } public Action onShutdown { get; set; } public Action 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 userAccountPairings { get { if (m_UserPairings == null) m_UserPairings = new List(); 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; } } public bool isInBatchMode { get; set; } #if UNITY_EDITOR public bool isInPlayMode { get; set; } = true; public bool isPaused { get; set; } public bool isEditorActive { get; set; } = true; public Func 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 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 m_EventBuffer = new NativeArray(kDefaultEventBufferSize, Allocator.Persistent); private List m_UserPairings; private List> m_NewDeviceDiscoveries; private List> m_DeviceCommandCallbacks; private object m_Lock = new object(); private double m_CurrentTimeOffsetToRealtimeSinceStartup; private Func m_UnityRemoteMessageHandler; #if UNITY_ANALYTICS || UNITY_EDITOR public Action onRegisterAnalyticsEvent { get; set; } public Action 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 } }