Skip to content

Commit d1489a6

Browse files
FIX: IndexOutOfRangeException when enabling InputActionMap. (ISXB-1767) (#2296)
Co-authored-by: Darren-Kelly-Unity <118264423+Darren-Kelly-Unity@users.noreply.github.com> Co-authored-by: Darren Kelly <darren.kelly@unity3d.com>
1 parent 1532d3f commit d1489a6

File tree

3 files changed

+82
-1
lines changed

3 files changed

+82
-1
lines changed

Assets/Tests/InputSystem/CoreTests_Actions.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12581,4 +12581,67 @@ public void Actions_ActionMapDisabledDuringOnAfterSerialization()
1258112581
Assert.That(map.enabled, Is.True);
1258212582
Assert.That(map.FindAction("MyAction", true).enabled, Is.True);
1258312583
}
12584+
12585+
// Verifies that a multi-control-scheme project with a disconnected device that attempts to enable the associated
12586+
// actions via an action cancellation callback doesn't result in IndexOutOfBoundsException, and that the binding
12587+
// will be restored upon device reconnect. We use the same bindings as the associated ISXB issue to make sure
12588+
// we test the exact same scenario.
12589+
[Test, Description("https://jira.unity3d.com/browse/ISXB-1767")]
12590+
public void Actions_CanHandleDeviceDisconnectWithControlSchemesAndReconnect()
12591+
{
12592+
int started = 0;
12593+
int performed = 0;
12594+
int canceled = 0;
12595+
12596+
// Create an input action asset object.
12597+
var actions = ScriptableObject.CreateInstance<InputActionAsset>();
12598+
12599+
// These control schemes are critical to this test. Without them the exception won't happen.
12600+
var keyboardScheme = actions.AddControlScheme("Keyboard").WithRequiredDevice<Keyboard>();
12601+
var gamepadScheme = actions.AddControlScheme("Gamepad").WithRequiredDevice<Gamepad>();
12602+
12603+
// Create a single action map since its sufficient for the scenario.
12604+
var map = actions.AddActionMap("map");
12605+
12606+
var action = map.AddAction(name: "Toggle", InputActionType.Button);
12607+
action.AddBinding("<Gamepad>/leftTrigger");
12608+
action.started += context => ++ started;
12609+
action.performed += context => ++ performed;
12610+
action.canceled += (context) =>
12611+
{
12612+
// In reported issue, map state is changed from cancellation callback.
12613+
map.Disable();
12614+
map.Enable();
12615+
12616+
// This is not part of the bug reported in ISXB-1767 but extends the test coverage since
12617+
// it makes sure Disable() is safe after logically skipped Enable().
12618+
map.Disable();
12619+
map.Enable();
12620+
12621+
++canceled;
12622+
};
12623+
12624+
// Add a keyboard and a gamepad.
12625+
var keyboard = InputSystem.AddDevice<Keyboard>();
12626+
var gamepad = InputSystem.AddDevice<Gamepad>();
12627+
12628+
// Enable the map, press (and hold) the left trigger and assert action is firing.
12629+
map.Enable();
12630+
Press(gamepad.leftTrigger, queueEventOnly: true);
12631+
InputSystem.Update();
12632+
Assert.That(started, Is.EqualTo(1));
12633+
12634+
// Remove the gamepad device. This is consistent with event queue based removal (not kept on list).
12635+
InputSystem.RemoveDevice(gamepad);
12636+
InputSystem.Update();
12637+
Assert.That(canceled, Is.EqualTo(1));
12638+
12639+
// Reconnect the disconnected gamepad
12640+
InputSystem.AddDevice(gamepad);
12641+
12642+
// Interact again
12643+
Press(gamepad.leftTrigger, queueEventOnly: true);
12644+
InputSystem.Update();
12645+
Assert.That(started, Is.EqualTo(2));
12646+
}
1258412647
}

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ however, it has to be formatted properly to pass verification tests.
2121
- Align title font size with toolbar style in `Input Action` window.
2222
- Updated Action Properties headers to use colors consistent with GameObject component headers.
2323
- Fixed misaligned Virtual Cursor when changing resolution [ISXB-1119](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1119)
24+
- 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).
2425

2526
### Added
2627

Packages/com.unity.inputsystem/InputSystem/Actions/InputActionState.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,13 @@ private void EnableControls(int mapIndex, int controlStartIndex, int numControls
11531153
if (IsControlEnabled(controlIndex))
11541154
continue;
11551155

1156+
// We might end up here if an action map is enabled from e.g. an event processing callback such as
1157+
// InputAction.cancel event handler (ISXB-1767). In this case we must skip controls associated with
1158+
// a device that is not connected to the system (Have deviceIndex < 0). We check this here to not
1159+
// cause side effects if aborting later in the call-chain.
1160+
if (!controls[controlIndex].device.added)
1161+
continue;
1162+
11561163
var bindingIndex = controlIndexToBindingIndex[controlIndex];
11571164
var mapControlAndBindingIndex = ToCombinedMapAndControlAndBindingIndex(mapIndex, controlIndex, bindingIndex);
11581165

@@ -1474,8 +1481,18 @@ private void ProcessControlStateChange(int mapIndex, int controlIndex, int bindi
14741481
if (m_OnBeforeUpdateHooked)
14751482
bindingStatePtr->initialStateCheckPending = false;
14761483

1477-
// Store magnitude. We do this once and then only read it from here.
14781484
var control = controls[controlIndex];
1485+
1486+
// We might end up here if an action map is enabled from e.g. an event processing callback such as
1487+
// InputAction.cancel event handler (ISXB-1767). In this case we must skip controls associated with
1488+
// a device that is not connected to the system (Have deviceIndex < 0). We check this here to not
1489+
// cause side effects if aborting later in the call-chain.
1490+
if (control == null || !controls[controlIndex].device.added)
1491+
{
1492+
return;
1493+
}
1494+
1495+
// Store magnitude. We do this once and then only read it from here.
14791496
trigger.magnitude = control.CheckStateIsAtDefault() ? 0f : control.magnitude;
14801497
controlMagnitudes[controlIndex] = trigger.magnitude;
14811498

0 commit comments

Comments
 (0)