490 lines
18 KiB
C#
490 lines
18 KiB
C#
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;
|
|
}
|
|
}
|
|
|
|
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<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
|
|
}
|
|
}
|