#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