Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using System;
using System.Collections;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.TestTools;
using Object = UnityEngine.Object;

// Enters real play mode so that the engine runs every RuntimeInitializeOnLoadMethod in its own order.
// This deliberately does not derive from InputTestFixture, which replaces the runtime and global state.
internal class InputActionStatePlayModeTests
{
const string k_TestCategory = "Editor";
const string k_ArmedKey = "InputSystem.Tests.InputActionStatePlayModeTests.Armed";
const string k_EnabledAfterSceneLoadKey = "InputSystem.Tests.InputActionStatePlayModeTests.EnabledAfterSceneLoad";
const string k_SavedOptionsKey = "InputSystem.Tests.InputActionStatePlayModeTests.SavedOptions";
const string k_SavedOptionsEnabledKey = "InputSystem.Tests.InputActionStatePlayModeTests.SavedOptionsEnabled";

static readonly bool[] k_DisableDomainReloadValues = { false, true };

static InputAction s_EarlyAction;

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void EnableActionBeforeSceneLoad()
{
if (!SessionState.GetBool(k_ArmedKey, false))
return;

s_EarlyAction = new InputAction(binding: "<Keyboard>/space");
s_EarlyAction.Enable();
}

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void RecordActionAfterSceneLoad()
{
if (!SessionState.GetBool(k_ArmedKey, false))
return;

SessionState.SetBool(k_EnabledAfterSceneLoadKey, s_EarlyAction != null && s_EarlyAction.enabled);
}

[SetUp]
public void SetUp()
{
// Each domain reload during the test runs SetUp again, so only record the options once.
if (SessionState.GetInt(k_SavedOptionsKey, -1) != -1)
return;

// Unity 6000.0 reports no options while they are disabled, so enable them to read the stored ones.
var optionsEnabled = EditorSettings.enterPlayModeOptionsEnabled;
EditorSettings.enterPlayModeOptionsEnabled = true;
SessionState.SetInt(k_SavedOptionsKey, (int)EditorSettings.enterPlayModeOptions);
SessionState.SetBool(k_SavedOptionsEnabledKey, optionsEnabled);
EditorSettings.enterPlayModeOptionsEnabled = optionsEnabled;
}

[TearDown]
public void TearDown()
{
if (Application.isPlaying)
return;

Check warning on line 62 in Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs#L62

Added line #L62 was not covered by tests

EditorSettings.enterPlayModeOptionsEnabled = true;
EditorSettings.enterPlayModeOptions = (EnterPlayModeOptions)SessionState.GetInt(k_SavedOptionsKey, 0);
EditorSettings.enterPlayModeOptionsEnabled = SessionState.GetBool(k_SavedOptionsEnabledKey, false);

// Entering play mode saves the modified settings to disk, so save the restored ones too.
AssetDatabase.SaveAssets();
SessionState.EraseInt(k_SavedOptionsKey);
SessionState.EraseBool(k_SavedOptionsEnabledKey);
SessionState.EraseBool(k_ArmedKey);
SessionState.EraseBool(k_EnabledAfterSceneLoadKey);

s_EarlyAction?.Dispose();
s_EarlyAction = null;
}

static void SetDomainReloadDisabled(bool disabled)
{
// Unity 6000.0 ignores enterPlayModeOptions unless the options are enabled.
EditorSettings.enterPlayModeOptionsEnabled = true;
EditorSettings.enterPlayModeOptions = disabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1

The disableDomainReload=true case still reloads the domain with the checked-in Unity 6000.0 settings, leaving s_EarlyAction null after ExitPlayMode. The project sets enterPlayModeOptionsEnabled to false, and SetDomainReloadDisabled only changes enterPlayModeOptions. Unity 6000.0 ignores those options until enterPlayModeOptionsEnabled is enabled. The assertion on s_EarlyAction.enabled then throws, and the callback test also runs with the wrong reload configuration. Save enterPlayModeOptionsEnabled alongside the existing options. Enable it when configuring the tests. Restore it in TearDown.

🤖 Helpful? 👍/👎

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae4c058, the tests now enable the enter play mode options and save and restore the enabled flag too.

? EnterPlayModeOptions.DisableDomainReload | EnterPlayModeOptions.DisableSceneReload
: EnterPlayModeOptions.None;
}

static bool HasOnActionChangeCallback(Action<object, InputActionChange> callback)
{
var callbacks = InputActionState.s_GlobalState.onActionChange;
for (var i = 0; i < callbacks.length; ++i)
{
if (callbacks[i] == callback)
return true;
}
return false;
}

[UnityTest]
[Category(k_TestCategory)]
[Description("https://jira.unity3d.com/browse/UUM-151403")]
public IEnumerator Actions_EnabledBeforeSceneLoad_StayEnabled_WhenEnteringPlayMode(
[ValueSource(nameof(k_DisableDomainReloadValues))] bool disableDomainReload)
{
SetDomainReloadDisabled(disableDomainReload);
SessionState.SetBool(k_ArmedKey, true);

yield return new EnterPlayMode();

SessionState.SetBool(k_ArmedKey, false);
Assert.That(SessionState.GetBool(k_EnabledAfterSceneLoadKey, false), Is.True);

yield return new ExitPlayMode();

// Without a domain reload the action object survives, but exiting play mode must still clean up its state.
if (disableDomainReload)
Assert.That(s_EarlyAction.enabled, Is.False);
}

[UnityTest]
[Category(k_TestCategory)]
[Description("https://jira.unity3d.com/browse/UUM-151403")]
public IEnumerator Actions_OnActionChangeCallbacksAddedInPlayMode_AreRemovedOnExit_WhenDomainReloadIsDisabled()
{
SetDomainReloadDisabled(true);

Action<object, InputActionChange> editModeCallback = (obj, change) => {};
Action<object, InputActionChange> playModeCallback = (obj, change) => {};
var survivingListener = ScriptableObject.CreateInstance<ActionChangeListener>();
survivingListener.hideFlags = HideFlags.HideAndDontSave;
var destroyedListener = ScriptableObject.CreateInstance<ActionChangeListener>();
destroyedListener.hideFlags = HideFlags.HideAndDontSave;
Action<object, InputActionChange> survivingListenerCallback = survivingListener.OnActionChange;
Action<object, InputActionChange> destroyedListenerCallback = destroyedListener.OnActionChange;
InputSystem.onActionChange += editModeCallback;
try
{
yield return new EnterPlayMode();

Assert.That(HasOnActionChangeCallback(editModeCallback), Is.True);
InputSystem.onActionChange += playModeCallback;

// Like an editor window opened during play mode, which stays open after it.
InputSystem.onActionChange += survivingListenerCallback;

// Like a component of a play mode GameObject, which is destroyed on exit.
InputSystem.onActionChange += destroyedListenerCallback;
Object.DestroyImmediate(destroyedListener);

yield return new ExitPlayMode();

Assert.That(HasOnActionChangeCallback(editModeCallback), Is.True);
Assert.That(HasOnActionChangeCallback(playModeCallback), Is.False);
Assert.That(HasOnActionChangeCallback(survivingListenerCallback), Is.True);
Assert.That(HasOnActionChangeCallback(destroyedListenerCallback), Is.False);
}
finally
{
InputSystem.onActionChange -= editModeCallback;
InputSystem.onActionChange -= playModeCallback;
InputSystem.onActionChange -= survivingListenerCallback;
InputSystem.onActionChange -= destroyedListenerCallback;
Object.DestroyImmediate(survivingListener);
}
}

class ActionChangeListener : ScriptableObject
{
public void OnActionChange(object actionOrMap, InputActionChange change)
{
}

Check warning on line 171 in Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

Assets/Tests/InputSystem.Editor/InputActionStatePlayModeTests.cs#L170-L171

Added lines #L170 - L171 were not covered by tests
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

- Fixed polling for interaction phases (such as `WasPerformedThisFrame`) being missed during `Update` due to interaction timeouts sometimes occurring during the later before-render period when a before-render device (such as an XR HMD) is added. This means that timeouts and interactions like Hold may now wait to perform until the next frame instead of the earliest chance during before-render. [UUM-147719](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-147719)
- Fixed touch input from Unity Remote 5 being ignored in Unity 6000.5 and newer. [UUM-155327](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-155327)
- Fixed actions enabled during startup, for example in an Entities system's `OnCreate`, being disabled when entering Play Mode with domain reload disabled and in Player builds. [UUM-151403](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-151403)

## [1.20.1] - 2026-10-05

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,8 @@ internal static void OnPlayModeChange(PlayModeStateChange change)

InputSystem.s_Manager.m_ExitEditModeTime = s_DomainStateManager.exitEditModeTime;
InputSystem.s_Manager.m_EnterPlayModeTime = 0;

InputActionState.RecordCallbacksBeforePlayMode();
break;

case PlayModeStateChange.EnteredPlayMode:
Expand All @@ -524,6 +526,7 @@ internal static void OnPlayModeChange(PlayModeStateChange change)

// Nuke all InputActionMapStates. Releases their unmanaged memory.
InputActionState.DestroyAllActionMapStates();
InputActionState.RemoveCallbacksAddedInPlayMode();

// Clear the Action reference from all InputActionReference objects
InputActionReference.InvalidateAll();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4499,25 +4499,56 @@ internal struct GlobalState
internal InlinedArray<GCHandle> globalList;
internal CallbackArray<Action<object, InputActionChange>> onActionChange;
internal CallbackArray<Action<object>> onActionControlsChanged;
#if UNITY_EDITOR
internal Action<object, InputActionChange>[] onActionChangeBeforePlayMode;
internal Action<object>[] onActionControlsChangedBeforePlayMode;
#endif
}

internal static GlobalState s_GlobalState;

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void InitializeGlobalActionState()
{
#if UNITY_EDITOR
// Appears we shouldn't really reset globals in case the domain reload is enabled.
// This is because in that case, we've just had the whole system init'ed via static ctors
// Moreover, later in GlobalInialize we skip initialization specifically in this case.
if (!(InputSystem.s_IsDomainReloadDisabled?.Invoke() ?? false))
internal static void RecordCallbacksBeforePlayMode()
{
s_GlobalState.onActionChangeBeforePlayMode = ToArray(ref s_GlobalState.onActionChange);
s_GlobalState.onActionControlsChangedBeforePlayMode = ToArray(ref s_GlobalState.onActionControlsChanged);
}

// Without a domain reload, callbacks added during play mode would otherwise outlive it.
internal static void RemoveCallbacksAddedInPlayMode()
{
// Nothing was recorded if a domain reload happened since, and that already cleared the callbacks.
if (s_GlobalState.onActionChangeBeforePlayMode == null)
return;
#endif

ResetGlobals();
s_GlobalState = default;
RemovePlayModeCallbacks(ref s_GlobalState.onActionChange, s_GlobalState.onActionChangeBeforePlayMode);
RemovePlayModeCallbacks(ref s_GlobalState.onActionControlsChanged, s_GlobalState.onActionControlsChangedBeforePlayMode);
s_GlobalState.onActionChangeBeforePlayMode = null;
s_GlobalState.onActionControlsChangedBeforePlayMode = null;
}

private static TDelegate[] ToArray<TDelegate>(ref CallbackArray<TDelegate> callbacks)
where TDelegate : Delegate
{
var result = new TDelegate[callbacks.length];
for (var i = 0; i < callbacks.length; ++i)
result[i] = callbacks[i];
return result;
}

private static void RemovePlayModeCallbacks<TDelegate>(ref CallbackArray<TDelegate> callbacks, TDelegate[] callbacksBeforePlayMode)
where TDelegate : Delegate
{
for (var i = callbacks.length - 1; i >= 0; --i)
{
// Play mode objects are destroyed by now, but editor objects such as open windows survive play mode.
var target = callbacks[i].Target as UnityEngine.Object;
if (Array.IndexOf(callbacksBeforePlayMode, callbacks[i]) < 0 && target == null)
callbacks.RemoveCallback(callbacks[i]);
}
}
#endif

internal static ISavedState SaveAndResetState()
{
// Save current state
Expand Down
Loading