From 529b2d27ee9f0e75623c7a23bb872bf4e399f169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kan=20Sidenvall?= Date: Thu, 27 Nov 2025 13:53:10 +0100 Subject: [PATCH 1/8] Added test case replicating the exact scenario that cause the reported issue. --- Assets/Tests/InputSystem/CoreTests_Actions.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index adf5f74c45..6680e9000e 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -17,6 +17,7 @@ using UnityEngine.InputSystem.Processors; using UnityEngine.InputSystem.Utilities; using UnityEngine.InputSystem.XInput; +using UnityEngine.PlayerLoop; using UnityEngine.Profiling; using UnityEngine.TestTools; using UnityEngine.TestTools.Utils; @@ -12581,4 +12582,51 @@ public void Actions_ActionMapDisabledDuringOnAfterSerialization() Assert.That(map.enabled, Is.True); Assert.That(map.FindAction("MyAction", true).enabled, Is.True); } + + // Verifies that a multi-control-scheme project with a disconnected device that attempts to enable the associated + // actions via an action cancellation callback doesn't result in IndexOutOfBoundsException, and that the binding + // will be restored upon device reconnect. We use the same bindings as the associated ISXB issue to make sure + // we test the exact same scenario. + [Test, Description("https://jira.unity3d.com/browse/ISXB-1767")] + public void Actions_CanHandleDeviceDisconnectWithControlSchemesAndReconnect() + { + int started = 0; + int performed = 0; + int canceled = 0; + + // Create an input action asset object. + var actions = ScriptableObject.CreateInstance(); + + // These control schemes are critical to this test. Without them the exception won't happen. + var keyboardScheme = actions.AddControlScheme("Keyboard").WithRequiredDevice(); + var gamepadScheme = actions.AddControlScheme("Gamepad").WithRequiredDevice(); + + // Create a single action map since its sufficient for the scenario. + var map = actions.AddActionMap("map"); + + var action = map.AddAction(name: "Toggle", InputActionType.Button); + action.AddBinding("/leftTrigger"); + action.started += context => ++ started; + action.performed += context => ++ performed; + action.canceled += (context) => + { + // In reported issue, map state is changed from cancellation callback. + map.Disable(); + map.Enable(); + ++canceled; + }; + + // Add a keyboard and a gamepad. + var keyboard = InputSystem.AddDevice(); + var gamepad = InputSystem.AddDevice(); + + // Enable the map, press (and hold) the left trigger and assert action is firing. + map.Enable(); + Press(gamepad.leftTrigger, queueEventOnly: true); + InputSystem.Update(); + Assert.That(started, Is.EqualTo(1)); + + // Remove the gamepad device. This is consistent with event queue based removal (not kept on list). + InputSystem.RemoveDevice(gamepad); + } } From 04fa0624d5a21ddc1c5bdbea6d5cdf69049e5019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kan=20Sidenvall?= Date: Thu, 27 Nov 2025 14:12:49 +0100 Subject: [PATCH 2/8] Updated CHANGELOG.md --- Packages/com.unity.inputsystem/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 49fb80f2a1..cc99ee2201 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -10,7 +10,8 @@ however, it has to be formatted properly to pass verification tests. ## [Unreleased] - yyyy-mm-dd - +### Fixed +- Fixed an issue where `IndexOutOfRangeException` was thrown from `InputManagerStateMonitors.AddStateChangeMonitor` when attempting to enable an action map from within an `InputAction.cancel` callback when using control schemes. (ISXB-1767). ## [1.17.0] - 2025-11-25 From 6a11944b8ffb349dbf54c60c4ca038184829974e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kan=20Sidenvall?= Date: Thu, 27 Nov 2025 14:33:49 +0100 Subject: [PATCH 3/8] Tentative fix --- .../InputSystem/Actions/InputActionState.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs index 82d6fe1082..9c922247ea 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs @@ -1153,6 +1153,9 @@ private void EnableControls(int mapIndex, int controlStartIndex, int numControls if (IsControlEnabled(controlIndex)) continue; + if (controls[controlIndex].device.m_DeviceIndex < 0) + return; + var bindingIndex = controlIndexToBindingIndex[controlIndex]; var mapControlAndBindingIndex = ToCombinedMapAndControlAndBindingIndex(mapIndex, controlIndex, bindingIndex); From d4b0ba4a52c859568ffa5e754b18dabfe08aa750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akan=20Sidenvall?= Date: Fri, 28 Nov 2025 09:04:28 +0100 Subject: [PATCH 4/8] Made bug fix use existing check, made test case stronger to verify that disabling controls doesn't require the same check. --- Assets/Tests/InputSystem/CoreTests_Actions.cs | 16 ++++++++++++++++ .../InputSystem/Actions/InputActionState.cs | 8 ++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index 6680e9000e..c2bd4ae3b2 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -12613,6 +12613,12 @@ public void Actions_CanHandleDeviceDisconnectWithControlSchemesAndReconnect() // In reported issue, map state is changed from cancellation callback. map.Disable(); map.Enable(); + + // This is not part of the bug reported in ISXB-1767 but extends the test coverage since + // it makes sure Disable() is safe after logically skipped Enable(). + map.Disable(); + map.Enable(); + ++canceled; }; @@ -12628,5 +12634,15 @@ public void Actions_CanHandleDeviceDisconnectWithControlSchemesAndReconnect() // Remove the gamepad device. This is consistent with event queue based removal (not kept on list). InputSystem.RemoveDevice(gamepad); + InputSystem.Update(); + Assert.That(canceled, Is.EqualTo(1)); + + // Reconnect the disconnected gamepad + InputSystem.AddDevice(gamepad); + + // Interact again + Press(gamepad.leftTrigger, queueEventOnly: true); + InputSystem.Update(); + Assert.That(started, Is.EqualTo(2)); } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs index 9c922247ea..cc45fb4243 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs @@ -1153,8 +1153,12 @@ private void EnableControls(int mapIndex, int controlStartIndex, int numControls if (IsControlEnabled(controlIndex)) continue; - if (controls[controlIndex].device.m_DeviceIndex < 0) - return; + // We might en up here if an action map is enabled from e.g. an event processing callback such as + // InputAction.cancel event handler (ISXB-1766). In this case we must skip controls associated with + // a device that is not connected to the system (Have deviceIndex < 0). We check this here to not + // cause side effects if aborting later in the call-chain. + if (!controls[controlIndex].device.added) + continue; var bindingIndex = controlIndexToBindingIndex[controlIndex]; var mapControlAndBindingIndex = ToCombinedMapAndControlAndBindingIndex(mapIndex, controlIndex, bindingIndex); From 5ac12921a07f9bb1d9a761e992c66c52980f440d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kan=20Sidenvall?= Date: Fri, 28 Nov 2025 09:19:06 +0100 Subject: [PATCH 5/8] Update Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs --- .../InputSystem/Actions/InputActionState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs index cc45fb4243..b6465602f7 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs @@ -1153,7 +1153,7 @@ private void EnableControls(int mapIndex, int controlStartIndex, int numControls if (IsControlEnabled(controlIndex)) continue; - // We might en up here if an action map is enabled from e.g. an event processing callback such as + // We might end up here if an action map is enabled from e.g. an event processing callback such as // InputAction.cancel event handler (ISXB-1766). In this case we must skip controls associated with // a device that is not connected to the system (Have deviceIndex < 0). We check this here to not // cause side effects if aborting later in the call-chain. From f9391714550cc084aa1a102a8029dc6e3344314b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kan=20Sidenvall?= Date: Fri, 28 Nov 2025 11:02:55 +0100 Subject: [PATCH 6/8] Update Assets/Tests/InputSystem/CoreTests_Actions.cs --- Assets/Tests/InputSystem/CoreTests_Actions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index c2bd4ae3b2..322a50a250 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -17,7 +17,6 @@ using UnityEngine.InputSystem.Processors; using UnityEngine.InputSystem.Utilities; using UnityEngine.InputSystem.XInput; -using UnityEngine.PlayerLoop; using UnityEngine.Profiling; using UnityEngine.TestTools; using UnityEngine.TestTools.Utils; From 4c9cec576832b0d5a670019bf559844431e874ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akan=20Sidenvall?= Date: Fri, 28 Nov 2025 11:03:40 +0100 Subject: [PATCH 7/8] Removed accidental using --- Assets/Tests/InputSystem/CoreTests_Actions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index c2bd4ae3b2..322a50a250 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -17,7 +17,6 @@ using UnityEngine.InputSystem.Processors; using UnityEngine.InputSystem.Utilities; using UnityEngine.InputSystem.XInput; -using UnityEngine.PlayerLoop; using UnityEngine.Profiling; using UnityEngine.TestTools; using UnityEngine.TestTools.Utils; From 870b1e9c8eb518d02474a78007f758223efb12bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kan=20Sidenvall?= Date: Fri, 28 Nov 2025 12:33:26 +0100 Subject: [PATCH 8/8] Update Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs --- .../InputSystem/Actions/InputActionState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs index b6465602f7..dc749a5803 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs @@ -1154,7 +1154,7 @@ private void EnableControls(int mapIndex, int controlStartIndex, int numControls continue; // We might end up here if an action map is enabled from e.g. an event processing callback such as - // InputAction.cancel event handler (ISXB-1766). In this case we must skip controls associated with + // InputAction.cancel event handler (ISXB-1767). In this case we must skip controls associated with // a device that is not connected to the system (Have deviceIndex < 0). We check this here to not // cause side effects if aborting later in the call-chain. if (!controls[controlIndex].device.added)