using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine.InputSystem.LowLevel;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
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);
public bool hasFocus => m_HasFocus;
~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 (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;
if (onUpdate != null)
{
var buffer = new InputEventBuffer(
(InputEvent*)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(m_EventBuffer),
m_EventCount, m_EventWritePosition, m_EventBuffer.Length);
onUpdate(type, ref buffer);
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;
}
dontAdvanceTimeNextDynamicUpdate = false;
}
}
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 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; }
public Action onDeviceDiscovered { get; set; }
public Action onShutdown { get; set; }
public Action onPlayerFocusChanged { get; set; }
public float pollingFrequency { get; set; }
public double currentTime { get; set; }
public double currentTimeForFixedUpdate { get; set; }
public float unscaledGameTime { get; set; } = 1;
public double advanceTimeEachDynamicUpdate { get; set; } = 1.0 / 60;
public bool dontAdvanceTimeNextDynamicUpdate { get; set; }
public bool runInBackground { get; set; } = false;
public ScreenOrientation screenOrientation { set; get; } = ScreenOrientation.Portrait;
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 Action onPlayModeChanged { get; set; }
public Action onProjectChange { get; set; }
#endif
public int eventCount => m_EventCount;
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(1024 * 1024, Allocator.Persistent);
private List m_UserPairings;
private List> m_NewDeviceDiscoveries;
private List> m_DeviceCommandCallbacks;
private object m_Lock = new object();
private double m_CurrentTimeOffsetToRealtimeSinceStartup;
#if UNITY_ANALYTICS || UNITY_EDITOR
public Action onRegisterAnalyticsEvent { get; set; }
public Action onSendAnalyticsEvent { get; set; }
public void RegisterAnalyticsEvent(string name, int maxPerHour, int maxPropertiesPerEvent)
{
onRegisterAnalyticsEvent?.Invoke(name, maxPerHour, maxPropertiesPerEvent);
}
public void SendAnalyticsEvent(string name, object data)
{
onSendAnalyticsEvent?.Invoke(name, data);
}
#endif // UNITY_ANALYTICS || UNITY_EDITOR
}
}