#if UNITY_EDITOR using System; using System.Collections.Generic; using Unity.Profiling; using UnityEditor; using UnityEditor.Networking.PlayerConnection; using UnityEditorInternal; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Users; using UnityEngine.InputSystem.Utilities; namespace UnityEngine.InputSystem.Editor { /// /// Handles Editor-specific initialization and lifecycle management for the Input System. /// [InitializeOnLoad] internal static class InputSystemEditorInitializer { private static InputSystemStateManager s_DomainStateManager; internal static InputSystemStateManager domainStateManager => s_DomainStateManager; static readonly ProfilerMarker k_InputInitializeInEditorMarker = new ProfilerMarker("InputSystem.InitializeInEditor"); static InputSystemEditorInitializer() { OnGlobalInitialize(calledFromCtor: true); } #region Editor Callbacks for NativeInputRuntime // Track AssemblyReloadEvents callbacks so we can unregister them private static readonly Dictionary s_AssemblyReloadCallbacks = new Dictionary(); private static void RegisterBeforeAssemblyReload(Action action) { if (s_AssemblyReloadCallbacks.ContainsKey(action)) return; AssemblyReloadEvents.AssemblyReloadCallback callback = () => action(); s_AssemblyReloadCallbacks[action] = callback; AssemblyReloadEvents.beforeAssemblyReload += callback; } private static void UnregisterBeforeAssemblyReload(Action action) { if (s_AssemblyReloadCallbacks.TryGetValue(action, out var callback)) { AssemblyReloadEvents.beforeAssemblyReload -= callback; s_AssemblyReloadCallbacks.Remove(action); } } private static void RegisterWantsToQuit(Func handler) { EditorApplication.wantsToQuit += handler; } private static void UnregisterWantsToQuit(Func handler) { EditorApplication.wantsToQuit -= handler; } private static void SendEditorAnalytic(InputAnalytics.IInputAnalytic analytic) { #if ENABLE_CLOUD_SERVICES_ANALYTICS #if UNITY_2023_2_OR_NEWER EditorAnalytics.SendAnalytic(analytic); #elif UNITY_INPUT_SYSTEM_ENABLE_ANALYTICS || UNITY_2023_1_OR_NEWER var info = analytic.info; EditorAnalytics.RegisterEventWithLimit(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements, InputAnalytics.kVendorKey); EditorAnalytics.SendEventWithLimit(info.Name, analytic); #endif #endif } #endregion private static void RegisterSetupApiUsage(InputAnalytics.AuthoringApi authoringApi) { InputExitPlayModeAnalytic.Register(authoringApi); } private static void SuppressSetupAnalytics(bool suppress) { InputExitPlayModeAnalytic.suppress = suppress; } #if UNITY_INPUT_SYSTEM_ENABLE_UI || PACKAGE_DOCS_GENERATION private static void OnUIInputModuleReset(UnityEngine.InputSystem.UI.InputSystemUIInputModule module) { var asset = (InputActionAsset)AssetDatabase.LoadAssetAtPath( PlayerInputEditor.kDefaultInputActionsAssetPath, typeof(InputActionAsset)); if (asset != null) { UI.Editor.InputSystemUIInputModuleEditor.ReassignActions(module, asset); } } #endif internal static bool IsDomainReloadDisabledForPlayMode() { #if !ENABLE_CORECLR if (!EditorSettings.enterPlayModeOptionsEnabled || (EditorSettings.enterPlayModeOptions & EnterPlayModeOptions.DisableDomainReload) == 0) return false; return true; #else return false; #endif } /// /// Called from InputSystem.GlobalInitialize via the s_EditorGlobalInitializeCallback. /// Handles the dual-call initialization pattern for Domain Reload disabled mode. /// private static void OnGlobalInitialize(bool calledFromCtor) { // When Domain Reloads are enabled, initialization is handled by [InitializeOnLoad] // via the static constructor. When DRs are disabled, the static ctor doesn't re-fire // on play mode entry, so we need RuntimeInitialize (calledFromCtor=false) to handle it. // The static ctor always fires on actual domain reload regardless. if (calledFromCtor || IsDomainReloadDisabledForPlayMode()) { InitializeInEditor(calledFromCtor); } } internal static void InitializeInEditor(bool calledFromCtor, IInputRuntime runtime = null) { k_InputInitializeInEditorMarker.Begin(); bool globalReset = calledFromCtor || !IsDomainReloadDisabledForPlayMode(); // We must initialize a new InputManager object first thing since other parts // of the init flow depend on it. if (globalReset) { if (InputSystem.s_Manager != null) InputSystem.s_Manager.Dispose(); // Settings object should get set by an actual InputSettings asset. InputSystem.s_Manager = InputManager.CreateAndInitialize(runtime ?? NativeInputRuntime.instance, null); EditorApplication.playModeStateChanged += OnEditorPlayModeStateChanged; EditorApplication.playModeStateChanged -= OnEditorPlayModeStateChanged; EditorApplication.playModeStateChanged += OnEditorPlayModeStateChanged; EditorApplication.projectChanged -= OnEditorProjectChanged; EditorApplication.projectChanged += OnEditorProjectChanged; InputSystem.s_Manager.runtime.onPlayModeChanged = InputSystem.OnPlayModeChange; InputSystem.s_Manager.runtime.onProjectChange = InputSystem.OnProjectChange; InputEditorUserSettings.s_Settings = new InputEditorUserSettings.SerializedState(); InputSystem.onSettingsChange += OnSettingsChanged; #if !UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION InputSystem.PerformDefaultPluginInitialization(); #endif } var existingStateManagers = Resources.FindObjectsOfTypeAll(); // Upgrade migration: package versions up to and including 1.18 stored this state in a // ScriptableObject named InputSystemObject. That instance survives the domain reload // triggered by a package upgrade, but the renamed InputSystemStateManager type won't match // it. Adopt the surviving legacy instance's state into a new state manager so connected // devices (and other state) are preserved across the upgrade instead of being lost - native // only re-reports devices once per editor process, so there is no in-process recovery // otherwise. See ISX-2569. if (globalReset && (existingStateManagers == null || existingStateManagers.Length == 0)) { var legacyObjects = Resources.FindObjectsOfTypeAll(); if (legacyObjects != null && legacyObjects.Length > 0) { var legacy = legacyObjects[0]; var migrated = ScriptableObject.CreateInstance(); migrated.hideFlags = HideFlags.HideAndDontSave; migrated.systemState = legacy.systemState; migrated.newInputBackendsCheckedAsEnabled = legacy.newInputBackendsCheckedAsEnabled; migrated.settings = legacy.settings; migrated.exitEditModeTime = legacy.exitEditModeTime; migrated.enterPlayModeTime = legacy.enterPlayModeTime; // The legacy instances are no longer needed; get rid of them so we don't migrate twice. for (var i = 0; i < legacyObjects.Length; ++i) Object.DestroyImmediate(legacyObjects[i]); existingStateManagers = new[] { migrated }; } } if (existingStateManagers != null && existingStateManagers.Length > 0) { if (globalReset) { ////FIXME: does not preserve action map state // If we're coming back out of a domain reload. We're restoring part of the // InputManager state here but we're still waiting from layout registrations // that happen during domain initialization. s_DomainStateManager = existingStateManagers[0]; InputSystem.s_Manager.RestoreStateWithoutDevices(s_DomainStateManager.systemState.managerState); InputDebuggerWindow.ReviveAfterDomainReload(); // Restore remoting state. InputSystem.remoteConnection = s_DomainStateManager.systemState.remoteConnection; InputSystem.SetUpRemoting(); InputSystem.s_Remote.RestoreState(s_DomainStateManager.systemState.remotingState, InputSystem.s_Manager); // Get s_Manager to restore devices on first input update. By that time we // should have all (possibly updated) layout information in place. InputSystem.s_Manager.m_SavedDeviceStates = s_DomainStateManager.systemState.managerState.devices; InputSystem.s_Manager.m_SavedAvailableDevices = s_DomainStateManager.systemState.managerState.availableDevices; // Restore editor settings. InputEditorUserSettings.s_Settings = s_DomainStateManager.systemState.userSettings; // Get rid of saved state. s_DomainStateManager.systemState = new InputSystemState(); } } else { s_DomainStateManager = ScriptableObject.CreateInstance(); s_DomainStateManager.hideFlags = HideFlags.HideAndDontSave; // See if we have a settings asset in our EditorBuildSettings. if (EditorBuildSettings.TryGetConfigObject(InputSettingsProvider.kEditorBuildSettingsConfigKey, out InputSettings settingsAsset)) { InputSystem.s_Manager.settings = settingsAsset; } // See if we have a saved actions object var savedActions = ProjectWideActionsBuildProvider.actionsToIncludeInPlayerBuild; if (savedActions != null) InputSystem.s_Manager.actions = savedActions; InputEditorUserSettings.Load(); SetUpEditorRemoting(); } Debug.Assert(InputSystem.settings != null); Debug.Assert(HasNativeObject(InputSystem.settings), "InputSettings has lost its native object"); UnityRemoteSupport.Initialize(); // If native backends for new input system aren't enabled, ask user whether we should enable them // (requires restart). We only ask once per session and don't ask when running in batch mode. // The warning is delayed to delay call (called a short while after the Asset are loaded, on Inspector // update) to make sure it doesn't pop up while the editor is still loading or assets are not fully loaded - // this would cancel the import of large assets that are dependent on the InputSystem package and import // it as a dependency. EditorApplication.delayCall += ShowRestartWarning; InputSystem.RunInitialUpdate(); InputSystem.EnableActions(); InitializeEditorHooks(); k_InputInitializeInEditorMarker.End(); } private static void InitializeEditorHooks() { RegisterPlayModeHooks(); RegisterAnalyticsHooks(); RegisterAssetDatabaseHooks(); #if UNITY_INPUT_SYSTEM_ENABLE_UI || PACKAGE_DOCS_GENERATION RegisterUIModuleHooks(); #endif RegisterNativeInputRuntimeHooks(); RegisterRemotingHooks(); RegisterAssemblyReloadHooks(); RegisterTestHooks(); } private static void RegisterPlayModeHooks() { InputSystem.s_ShouldEnableActions = ShouldEnableActions; InputSystem.s_OnPlayModeChangeCallback = change => OnPlayModeChange((PlayModeStateChange)change); InputSystem.s_OnProjectChangeCallback = OnProjectChange; InputSystem.s_IsDomainReloadDisabled = IsDomainReloadDisabledForPlayMode; InputSystem.s_EditorGlobalInitializeCallback = OnGlobalInitialize; InputSystem.s_HasNativeObjectCallback = HasNativeObject; InputSystem.s_Manager.m_AddDevicesNotSupportedByProject = () => InputEditorUserSettings.addDevicesNotSupportedByProject; } private static void RegisterAnalyticsHooks() { InputAnalytics.IsNewSystemBackendsEnabled = ShouldEnableActionsNewBackend; InputAnalytics.IsOldSystemBackendsEnabled = ShouldEnableActionsOldBackend; InputActionSetupExtensions.s_ApiUsageCallback = RegisterSetupApiUsage; InputActionSetupExtensions.s_SuppressAnalytics = SuppressSetupAnalytics; } private static void RegisterAssetDatabaseHooks() { InputSystem.s_OnActionsChanging = ValidateAndTrackActions; InputActionAsset.s_OnMarkAsDirty = DirtyAssetTracker.TrackDirtyInputActionAsset; InputManager.s_GetProjectWideActions = () => ProjectWideActionsBuildProvider.actionsToIncludeInPlayerBuild; InputActionReference.s_CheckImmutableReference = CheckImmutableInputActionReference; InputSettings.s_GetIsInspectorIndented = () => EditorGUI.indentLevel > 0; } /// /// Prevents accidental mutation of the source asset if this is a /// persisted sub-asset within a .inputactions . /// private static void CheckImmutableInputActionReference(InputActionReference reference) { if (!AssetDatabase.IsSubAsset(reference)) return; var path = AssetDatabase.GetAssetPath(reference); if (path == null) return; var mainAsset = AssetDatabase.LoadMainAssetAtPath(path); if (!mainAsset || mainAsset is not InputActionAsset) return; throw new InvalidOperationException( "Attempting to modify an immutable InputActionReference instance " + "that is part of an .inputactions asset. This is not allowed since it would modify the source " + "asset in which the reference is serialized and potentially corrupt it. " + "Instead use InputActionReference.Create(action) to create a new mutable " + "in-memory instance or serialize it as a separate asset if the intent is for changes to " + "survive domain reloads."); } #if UNITY_INPUT_SYSTEM_ENABLE_UI || PACKAGE_DOCS_GENERATION private static void RegisterUIModuleHooks() { UnityEngine.InputSystem.UI.InputSystemUIInputModule.s_OnReset = OnUIInputModuleReset; } #endif private static void RegisterNativeInputRuntimeHooks() { if (InputRuntime.s_Instance is not NativeInputRuntime nativeRuntime) return; nativeRuntime.m_IsInPlayMode = () => EditorApplication.isPlaying; nativeRuntime.m_IsEditorPaused = () => EditorApplication.isPaused; nativeRuntime.m_IsEditorActive = () => InternalEditorUtility.isApplicationActive; nativeRuntime.m_RegisterWantsToQuit = RegisterWantsToQuit; nativeRuntime.m_UnregisterWantsToQuit = UnregisterWantsToQuit; nativeRuntime.m_SendEditorAnalytic = SendEditorAnalytic; } private static void RegisterRemotingHooks() { RemoteInputPlayerConnection.s_GetInstance = RemoteInputPlayerConnectionEditor.GetInstance; } private static void RegisterAssemblyReloadHooks() { EnhancedTouch.EnhancedTouchSupport.s_BeforeAssemblyReloadCallback = RegisterBeforeAssemblyReload; EnhancedTouch.EnhancedTouchSupport.s_UnregisterBeforeAssemblyReloadCallback = UnregisterBeforeAssemblyReload; } private static void RegisterTestHooks() { InputSystemTestHooks.s_TestHookInitializeForPlayModeTests = TestHook_InitializeForPlayModeTests; #if !ENABLE_CORECLR InputSystemTestHooks.s_TestHookSimulateDomainReload = TestHook_SimulateDomainReload; #endif InputSystemTestHooks.s_TestHookEditorCleanup = TestHook_EditorCleanup; } private static void SetUpEditorRemoting() { InputSystem.s_Remote = new InputRemoting(InputSystem.s_Manager); EditorApplication.delayCall += SetUpRemotingInternal; } private static void SetUpRemotingInternal() { if (InputSystem.remoteConnection == null) { InputSystem.remoteConnection = RemoteInputPlayerConnection.instance; InputSystem.remoteConnection.Bind(EditorConnection.instance, false); } InputSystem.s_Remote.Subscribe(InputSystem.remoteConnection); InputSystem.remoteConnection.Subscribe(InputSystem.s_Remote); } private static void OnSettingsChanged() { var settings = InputSystem.settings; if (settings != null && !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(settings))) { EditorBuildSettings.AddConfigObject(InputSettingsProvider.kEditorBuildSettingsConfigKey, settings, true); } } internal static void ValidateAndTrackActions(InputActionAsset value) { if (value != null) { if (!EditorUtility.IsPersistent(value)) throw new ArgumentException($"Assigning a non-persistent {nameof(InputActionAsset)} to this property is not allowed. The assigned asset needs to be persisted on disk inside the /Assets folder."); ProjectWideActionsBuildProvider.actionsToIncludeInPlayerBuild = value; } else { ProjectWideActionsBuildProvider.actionsToIncludeInPlayerBuild = null; } } internal static bool ShouldEnableActions() { return EditorApplication.isPlayingOrWillChangePlaymode; } internal static bool ShouldEnableActionsNewBackend() { return EditorPlayerSettingHelpers.newSystemBackendsEnabled; } internal static bool ShouldEnableActionsOldBackend() { return EditorPlayerSettingHelpers.oldSystemBackendsEnabled; } private static void ShowRestartWarning() { if (!s_DomainStateManager.newInputBackendsCheckedAsEnabled && !EditorPlayerSettingHelpers.newSystemBackendsEnabled && !Application.isBatchMode) { const string dialogText = "The new Input System Package is installed, but not configured to enable native device input, such as keyboard, mouse, or gamepad actions. " + "\n\nThe Active Input Handling parameter must be set to \"Input System Package (New)\", under Project Settings > Player." + "\n\nNote: Changing the active input handling requires to restart the Editor."; bool userChoseEnableAndRestart; #if UNITY_6000_3_OR_NEWER userChoseEnableAndRestart = EditorUtility.DisplayDialog( "Input System native platform backend not enabled", dialogText, "Enable & Restart", "Don't Enable", DialogOptOutDecisionType.ForThisSession, "RestartInstalledInputHandlingWarning"); #else userChoseEnableAndRestart = EditorUtility.DisplayDialog( "Input System native platform backend not enabled", dialogText, "Enable & Restart", "Don't Enable"); #endif if (userChoseEnableAndRestart) { EditorPlayerSettingHelpers.newSystemBackendsEnabled = true; EditorHelpers.RestartEditorAndRecompileScripts(); } } s_DomainStateManager.newInputBackendsCheckedAsEnabled = true; EditorApplication.delayCall -= ShowRestartWarning; } private static void OnEditorPlayModeStateChanged(PlayModeStateChange change) { if (InputRuntime.s_Instance is NativeInputRuntime nativeRuntime) { nativeRuntime.DispatchPlayModeChange((InputPlayModeChange)change); } } private static void OnEditorProjectChanged() { if (InputRuntime.s_Instance is NativeInputRuntime nativeRuntime) { nativeRuntime.DispatchProjectChange(); } } internal static void OnPlayModeChange(PlayModeStateChange change) { switch (change) { case PlayModeStateChange.ExitingEditMode: s_DomainStateManager.settings = JsonUtility.ToJson(InputSystem.settings); s_DomainStateManager.exitEditModeTime = InputRuntime.s_Instance.currentTime; s_DomainStateManager.enterPlayModeTime = 0; InputSystem.s_Manager.m_ExitEditModeTime = s_DomainStateManager.exitEditModeTime; InputSystem.s_Manager.m_EnterPlayModeTime = 0; break; case PlayModeStateChange.EnteredPlayMode: s_DomainStateManager.enterPlayModeTime = InputRuntime.s_Instance.currentTime; InputSystem.s_Manager.m_EnterPlayModeTime = s_DomainStateManager.enterPlayModeTime; InputSystem.s_Manager.SyncAllDevicesAfterEnteringPlayMode(); break; case PlayModeStateChange.ExitingPlayMode: InputSystem.s_Manager.LeavePlayMode(); break; ////TODO: also nuke all callbacks installed on InputActions and InputActionMaps ////REVIEW: is there any other cleanup work we want to before? should we automatically nuke //// InputDevices that have been created with AddDevice<> during play mode? case PlayModeStateChange.EnteredEditMode: InputSystem.DisableActions(false); // Nuke all InputUsers. InputUser.ResetGlobals(); // Nuke all InputActionMapStates. Releases their unmanaged memory. InputActionState.DestroyAllActionMapStates(); // Clear the Action reference from all InputActionReference objects InputActionReference.InvalidateAll(); // Restore settings. if (!string.IsNullOrEmpty(s_DomainStateManager.settings)) { JsonUtility.FromJsonOverwrite(s_DomainStateManager.settings, InputSystem.settings); s_DomainStateManager.settings = null; InputSystem.settings.OnChange(); } // Reload input assets marked as dirty from disk DirtyAssetTracker.ReloadDirtyAssets(); break; } } internal static void OnProjectChange() { ////TODO: use dirty count to find whether settings have actually changed // May have added, removed, moved, or renamed settings asset. Force a refresh // of the UI. InputSettingsProvider.ForceReload(); // Also, if the asset holding our current settings got deleted, switch back to a // temporary settings object. // NOTE: We access m_Settings directly here to make sure we're not running into asserts // from the settings getter checking it has a valid object. if (!HasNativeObject(InputSystem.s_Manager.settings)) { var newSettings = ScriptableObject.CreateInstance(); newSettings.hideFlags = HideFlags.HideAndDontSave; InputSystem.settings = newSettings; } } private static bool HasNativeObject(Object obj) { #if UNITY_6000_3_OR_NEWER return EditorUtility.EntityIdToObject(obj.GetEntityId()) != null; #else return EditorUtility.InstanceIDToObject(obj.GetInstanceID()) != null; #endif } #region Test Hooks private static void TestHook_InitializeForPlayModeTests(bool enableRemoting, IInputRuntime runtime) { InputSystem.s_Manager = InputManager.CreateAndInitialize(runtime, null); InputSystem.s_Manager.runtime.onPlayModeChanged = InputSystem.OnPlayModeChange; InputEditorUserSettings.s_Settings = new InputEditorUserSettings.SerializedState(); if (enableRemoting) InputSystem.SetUpRemoting(); #if !UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION InputSystem.s_PluginsInitialized = false; InputSystem.PerformDefaultPluginInitialization(); #endif } #if !ENABLE_CORECLR private static void TestHook_SimulateDomainReload(IInputRuntime runtime) { InputSystem.s_Manager.TestHook_RemoveDevicesForSimulatedDomainReload(); s_DomainStateManager.OnBeforeSerialize(); s_DomainStateManager = null; InputSystem.s_Manager = null; InputSystem.s_PluginsInitialized = false; InitializeInEditor(calledFromCtor: true, runtime); } #endif private static void TestHook_EditorCleanup() { EditorInputControlLayoutCache.Clear(); InputDeviceDebuggerWindow.s_OnToolbarGUIActions.Clear(); InputEditorUserSettings.s_Settings = new InputEditorUserSettings.SerializedState(); } #endregion } } #endif