using System; using System.Linq; using Unity.Collections.LowLevel.Unsafe; using UnityEngine.InputSystem.Utilities; using UnityEngineInternal.Input; #if UNITY_EDITOR using System.Reflection; using UnityEditor; using UnityEditorInternal; #endif // This should be the only file referencing the API at UnityEngineInternal.Input. namespace UnityEngine.InputSystem.LowLevel { /// /// Implements based on . /// internal class NativeInputRuntime : IInputRuntime { public static readonly NativeInputRuntime instance = new NativeInputRuntime(); public int AllocateDeviceId() { return NativeInputSystem.AllocateDeviceId(); } public void Update(InputUpdateType updateType) { NativeInputSystem.Update((NativeInputUpdateType)updateType); } public unsafe void QueueEvent(InputEvent* ptr) { NativeInputSystem.QueueInputEvent((IntPtr)ptr); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "False positive.")] public unsafe long DeviceCommand(int deviceId, InputDeviceCommand* commandPtr) { if (commandPtr == null) throw new ArgumentNullException(nameof(commandPtr)); return NativeInputSystem.IOCTL(deviceId, commandPtr->type, new IntPtr(commandPtr->payloadPtr), commandPtr->payloadSizeInBytes); } public unsafe InputUpdateDelegate onUpdate { get => m_OnUpdate; set { if (value != null) NativeInputSystem.onUpdate = (updateType, eventBufferPtr) => { var buffer = new InputEventBuffer((InputEvent*)eventBufferPtr->eventBuffer, eventBufferPtr->eventCount, sizeInBytes: eventBufferPtr->sizeInBytes, capacityInBytes: eventBufferPtr->capacityInBytes); try { value((InputUpdateType)updateType, ref buffer); } catch (Exception e) { // Always report the original exception first to confuse users less about what it the actual failure. Debug.LogException(e); Debug.LogError($"{e.GetType().Name} during event processing of {updateType} update; resetting event buffer"); buffer.Reset(); } if (buffer.eventCount > 0) { eventBufferPtr->eventCount = buffer.eventCount; eventBufferPtr->sizeInBytes = (int)buffer.sizeInBytes; eventBufferPtr->capacityInBytes = (int)buffer.capacityInBytes; eventBufferPtr->eventBuffer = NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(buffer.data); } else { eventBufferPtr->eventCount = 0; eventBufferPtr->sizeInBytes = 0; } }; else NativeInputSystem.onUpdate = null; m_OnUpdate = value; } } public Action onBeforeUpdate { get => m_OnBeforeUpdate; set { // This is stupid but the enum prevents us from jacking the delegate in directly. // This means we get a double dispatch here :( if (value != null) NativeInputSystem.onBeforeUpdate = updateType => value((InputUpdateType)updateType); else NativeInputSystem.onBeforeUpdate = null; m_OnBeforeUpdate = value; } } public Func onShouldRunUpdate { get => m_OnShouldRunUpdate; set { // This is stupid but the enum prevents us from jacking the delegate in directly. // This means we get a double dispatch here :( if (value != null) NativeInputSystem.onShouldRunUpdate = updateType => value((InputUpdateType)updateType); else NativeInputSystem.onShouldRunUpdate = null; m_OnShouldRunUpdate = value; } } #if UNITY_EDITOR private struct InputSystemPlayerLoopRunnerInitializationSystem {}; public Action onPlayerLoopInitialization { get => m_PlayerLoopInitialization; set { // This is a hot-fix for a critical problem in input system, case 1368559, case 1367556, case 1372830 // TODO move it to a proper native callback instead if (value != null) { // Inject ourselves directly to PlayerLoop.Initialization as first subsystem to run, // Use InputSystemPlayerLoopRunnerInitializationSystem as system type var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop(); var initStepIndex = playerLoop.subSystemList.IndexOf(x => x.type == typeof(PlayerLoop.Initialization)); if (initStepIndex >= 0) { var systems = playerLoop.subSystemList[initStepIndex].subSystemList; // Check if we're not already injected if (!systems.Select(x => x.type) .Contains(typeof(InputSystemPlayerLoopRunnerInitializationSystem))) { ArrayHelpers.InsertAt(ref systems, 0, new UnityEngine.LowLevel.PlayerLoopSystem { type = typeof(InputSystemPlayerLoopRunnerInitializationSystem), updateDelegate = () => m_PlayerLoopInitialization?.Invoke() }); playerLoop.subSystemList[initStepIndex].subSystemList = systems; UnityEngine.LowLevel.PlayerLoop.SetPlayerLoop(playerLoop); } } } m_PlayerLoopInitialization = value; } } #endif public Action onDeviceDiscovered { get => NativeInputSystem.onDeviceDiscovered; set => NativeInputSystem.onDeviceDiscovered = value; } public Action onShutdown { get => m_ShutdownMethod; set { if (value == null) { #if UNITY_EDITOR EditorApplication.wantsToQuit -= OnWantsToShutdown; #else Application.quitting -= OnShutdown; #endif } else if (m_ShutdownMethod == null) { #if UNITY_EDITOR EditorApplication.wantsToQuit += OnWantsToShutdown; #else Application.quitting += OnShutdown; #endif } m_ShutdownMethod = value; } } public Action onPlayerFocusChanged { get => m_FocusChangedMethod; set { if (value == null) Application.focusChanged -= OnFocusChanged; else if (m_FocusChangedMethod == null) Application.focusChanged += OnFocusChanged; m_FocusChangedMethod = value; } } public bool isPlayerFocused => Application.isFocused; public float pollingFrequency { get => m_PollingFrequency; set { m_PollingFrequency = value; NativeInputSystem.SetPollingFrequency(value); } } public double currentTime => NativeInputSystem.currentTime; ////REVIEW: this applies the offset, currentTime doesn't public double currentTimeForFixedUpdate => Time.fixedUnscaledTime + currentTimeOffsetToRealtimeSinceStartup; public double currentTimeOffsetToRealtimeSinceStartup => NativeInputSystem.currentTimeOffsetToRealtimeSinceStartup; public float unscaledGameTime => Time.unscaledTime; public bool runInBackground => Application.runInBackground || // certain platforms ignore the runInBackground flag and always run. Make sure we're // not running on one of those. // TODO: Add more platforms here as they're discovered. Application.platform == RuntimePlatform.PS5; private Action m_ShutdownMethod; private InputUpdateDelegate m_OnUpdate; private Action m_OnBeforeUpdate; private Func m_OnShouldRunUpdate; #if UNITY_EDITOR private Action m_PlayerLoopInitialization; #endif private float m_PollingFrequency = 60.0f; private bool m_DidCallOnShutdown = false; private void OnShutdown() { m_ShutdownMethod(); } private bool OnWantsToShutdown() { if (!m_DidCallOnShutdown) { // we should use `EditorApplication.quitting`, but that is too late // to send an analytics event, because Analytics is already shut down // at that point. So we use `EditorApplication.wantsToQuit`, and make sure // to only use the first time. This is currently only used for analytics, // and getting analytics before we actually shut downn in some cases is // better then never. OnShutdown(); m_DidCallOnShutdown = true; } return true; } private Action m_FocusChangedMethod; private void OnFocusChanged(bool focus) { m_FocusChangedMethod(focus); } public Vector2 screenSize => new Vector2(Screen.width, Screen.height); public ScreenOrientation screenOrientation => Screen.orientation; public bool isInBatchMode => Application.isBatchMode; #if UNITY_EDITOR public bool isInPlayMode => EditorApplication.isPlaying; public bool isPaused => EditorApplication.isPaused; public bool isEditorActive => InternalEditorUtility.isApplicationActive; public Func onUnityRemoteMessage { set { if (m_UnityRemoteMessageHandler == value) return; if (m_UnityRemoteMessageHandler != null) { var removeMethod = GetUnityRemoteAPIMethod("RemoveMessageHandler"); removeMethod?.Invoke(null, new[] { m_UnityRemoteMessageHandler }); m_UnityRemoteMessageHandler = null; } if (value != null) { var addMethod = GetUnityRemoteAPIMethod("AddMessageHandler"); addMethod?.Invoke(null, new[] { value }); m_UnityRemoteMessageHandler = value; } } } public void SetUnityRemoteGyroEnabled(bool value) { var setMethod = GetUnityRemoteAPIMethod("SetGyroEnabled"); setMethod?.Invoke(null, new object[] { value }); } public void SetUnityRemoteGyroUpdateInterval(float interval) { var setMethod = GetUnityRemoteAPIMethod("SetGyroUpdateInterval"); setMethod?.Invoke(null, new object[] { interval }); } private MethodInfo GetUnityRemoteAPIMethod(string methodName) { var editorAssembly = typeof(EditorApplication).Assembly; var genericRemoteClass = editorAssembly.GetType("UnityEditor.Remote.GenericRemote"); if (genericRemoteClass == null) return null; return genericRemoteClass.GetMethod(methodName); } private Func m_UnityRemoteMessageHandler; private Action m_OnPlayModeChanged; private Action m_OnProjectChanged; private void OnPlayModeStateChanged(PlayModeStateChange value) { m_OnPlayModeChanged(value); } private void OnProjectChanged() { m_OnProjectChanged(); } public Action onPlayModeChanged { get => m_OnPlayModeChanged; set { if (value == null) EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; else if (m_OnPlayModeChanged == null) EditorApplication.playModeStateChanged += OnPlayModeStateChanged; m_OnPlayModeChanged = value; } } public Action onProjectChange { get => m_OnProjectChanged; set { if (value == null) EditorApplication.projectChanged -= OnProjectChanged; else if (m_OnProjectChanged == null) EditorApplication.projectChanged += OnProjectChanged; m_OnProjectChanged = value; } } #endif // UNITY_EDITOR public void RegisterAnalyticsEvent(string name, int maxPerHour, int maxPropertiesPerEvent) { #if UNITY_ANALYTICS const string vendorKey = "unity.input"; #if UNITY_EDITOR EditorAnalytics.RegisterEventWithLimit(name, maxPerHour, maxPropertiesPerEvent, vendorKey); #else Analytics.Analytics.RegisterEvent(name, maxPerHour, maxPropertiesPerEvent, vendorKey); #endif // UNITY_EDITOR #endif // UNITY_ANALYTICS } public void SendAnalyticsEvent(string name, object data) { #if UNITY_ANALYTICS #if UNITY_EDITOR EditorAnalytics.SendEventWithLimit(name, data); #else Analytics.Analytics.SendEvent(name, data); #endif // UNITY_EDITOR #endif // UNITY_ANALYTICS } } }