Repository: Leafwing-Studios/leafwing-input-manager (rust)

Issue:
Gamepad actions are sent to all related `ActionState` components, regardless of restrictions placed by `InputMap::set_gamepad()`
## Version

Release 0.13.3

## Operating system & version

* Kubuntu 23.10 x86_64, with Kernel 6.5.0-26-generic

## What you did

Requires the use of 2 gamepads. 

0. Clone the repository and 
1. Execute `cargo run --example register_gamepads`. 
2. Once the window opens, register both controllers with L+R bumpers.
3. Press the south face button on either controller.

## What you expected to happen

The log should display a single instance of the "Player # jumped!" message (where '#' is number of the gamepad used).

## What actually happened

The log instead displays the message a number of times equal to the number of registered gamepads. 

## Additional information

This seems to result from a deeper problem. The function `InputMap::set_gamepad(..)` is intended to restrict the gamepads associated with an InputMap and ActionState component bundle, but it seems to have no impact in this case.

Pull request description:
Fixed gamepad filtering (alternate branch)
Fixes #500 (again), as an alternative to the solution provided in #501.
This also adds a new test group (`tests/multiple_gamepads.rs`) and (hopefully) clarifies the documentation for `InputMap.set_gamepad()`.

Patch:
diff --git a/RELEASES.md b/RELEASES.md
index 014e37d..a9758eb 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -6,6 +6,10 @@
 
 - Removed `Direction` type. Use `bevy::math::primitives::Direction2d`.
 
+### Bugs
+
+- fixed a bug in `InputStreams::button_pressed()` where unrelated gamepads were not filtered out when an `associated_gamepad` is defined.
+
 ## Version 0.13.3
 
 ### Bugs
diff --git a/examples/default_controls.rs b/examples/default_controls.rs
index 0c71b20..7cadecf 100644
--- a/examples/default_controls.rs
+++ b/examples/default_controls.rs
@@ -1,7 +1,7 @@
 //! Demonstrates how to create default controls for an `Actionlike` and add it to an `InputMap`
 
 use bevy::prelude::*;
-use leafwing_input_manager::{prelude::*, user_input::InputKind};
+use leafwing_input_manager::prelude::*;
 
 fn main() {
     App::new()
diff --git a/src/input_map.rs b/src/input_map.rs
index 6eea66a..6aff586 100644
--- a/src/input_map.rs
+++ b/src/input_map.rs
@@ -261,6 +261,9 @@ impl<A: Actionlike> InputMap<A> {
 
     /// Assigns a particular [`Gamepad`] to the entity controlled by this input map
     ///
+    /// Use this when an [`InputMap`] should exclusively accept input from a
+    /// particular gamepad.
+    ///
     /// If this is not called, input from any connected gamepad will be used.
     /// The first matching non-zero input will be accepted,
     /// as determined by gamepad registration order.
diff --git a/src/input_streams.rs b/src/input_streams.rs
index 162266d..7ad45c0 100644
--- a/src/input_streams.rs
+++ b/src/input_streams.rs
@@ -102,16 +102,27 @@ impl<'a> InputStreams<'a> {
 
                 value < axis.negative_low || value > axis.positive_low
             }
-            InputKind::GamepadButton(button_type) => self
-                .associated_gamepad
-                .into_iter()
-                .chain(self.gamepads.iter())
-                .any(|gamepad| {
-                    self.gamepad_buttons.pressed(GamepadButton {
+            InputKind::GamepadButton(button_type) => {
+                if let Some(gamepad) = self.associated_gamepad {
+                    return self.gamepad_buttons.pressed(GamepadButton {
+                        gamepad,
+                        button_type,
+                    });
+                }
+
+                for gamepad in self.gamepads.iter() {
+                    if self.gamepad_buttons.pressed(GamepadButton {
                         gamepad,
                         button_type,
-                    })
-                }),
+                    }) {
+                        // Return early if ANY gamepad is pressing this button
+                        return true;
+                    }
+                }
+
+                // If we don't have the required data, fall back to false
+                false
+            }
             InputKind::PhysicalKey(keycode) => {
                 matches!(self.keycodes, Some(keycodes) if keycodes.pressed(keycode))
             }


Test patch:
diff --git a/tests/multiple_gamepads.rs b/tests/multiple_gamepads.rs
new file mode 100644
index 0000000..8c5cb5d
--- /dev/null
+++ b/tests/multiple_gamepads.rs
@@ -0,0 +1,123 @@
+use bevy::input::gamepad::{GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadInfo};
+use bevy::input::InputPlugin;
+use bevy::prelude::*;
+use leafwing_input_manager::prelude::*;
+
+#[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
+enum MyAction {
+    Jump,
+}
+
+fn create_test_app() -> App {
+    let mut app = App::new();
+    app.add_plugins(MinimalPlugins).add_plugins(InputPlugin);
+    app.add_plugins(InputManagerPlugin::<MyAction>::default());
+
+    let mut gamepad_events = app.world.resource_mut::<Events<GamepadEvent>>();
+    gamepad_events.send(GamepadEvent::Connection(GamepadConnectionEvent {
+        // Must be consistent with mocked events
+        gamepad: Gamepad { id: 1 },
+        connection: GamepadConnection::Connected(GamepadInfo {
+            name: "FirstController".into(),
+        }),
+    }));
+    gamepad_events.send(GamepadEvent::Connection(GamepadConnectionEvent {
+        // Must be consistent with mocked events
+        gamepad: Gamepad { id: 2 },
+        connection: GamepadConnection::Connected(GamepadInfo {
+            name: "SecondController".into(),
+        }),
+    }));
+
+    // Ensure the gamepads are picked up
+    app.update();
+    // Flush the gamepad connection events
+    app.update();
+
+    app
+}
+
+fn jump_button_press_event(gamepad: Gamepad) -> GamepadEvent {
+    use bevy::input::gamepad::GamepadButtonChangedEvent;
+
+    GamepadEvent::Button(GamepadButtonChangedEvent::new(
+        gamepad,
+        GamepadButtonType::South,
+        1.0,
+    ))
+}
+
+#[test]
+fn default_accepts_any() {
+    let mut app = create_test_app();
+
+    const FIRST_GAMEPAD: Gamepad = Gamepad { id: 1 };
+    const SECOND_GAMEPAD: Gamepad = Gamepad { id: 2 };
+
+    let input_map = InputMap::new([(MyAction::Jump, GamepadButtonType::South)]);
+    app.insert_resource(input_map);
+    app.init_resource::<ActionState<MyAction>>();
+
+    // When we press the Jump button...
+    let mut events = app.world.resource_mut::<Events<GamepadEvent>>();
+    events.send(jump_button_press_event(FIRST_GAMEPAD));
+    app.update();
+
+    // ... We should receive a Jump action!
+    let mut action_state = app.world.resource_mut::<ActionState<MyAction>>();
+    assert!(action_state.pressed(&MyAction::Jump));
+
+    action_state.release(&MyAction::Jump);
+    app.update();
+
+    // This is maintained for any gamepad.
+    let mut events = app.world.resource_mut::<Events<GamepadEvent>>();
+    events.send(jump_button_press_event(SECOND_GAMEPAD));
+    app.update();
+
+    let action_state = app.world.resource_mut::<ActionState<MyAction>>();
+    assert!(action_state.pressed(&MyAction::Jump));
+}
+
+#[test]
+fn accepts_preferred_gamepad() {
+    let mut app = create_test_app();
+
+    const PREFERRED_GAMEPAD: Gamepad = Gamepad { id: 2 };
+
+    let mut input_map = InputMap::new([(MyAction::Jump, GamepadButtonType::South)]);
+    input_map.set_gamepad(PREFERRED_GAMEPAD);
+    app.insert_resource(input_map);
+    app.init_resource::<ActionState<MyAction>>();
+
+    // When we press the Jump button...
+    let mut events = app.world.resource_mut::<Events<GamepadEvent>>();
+    events.send(jump_button_press_event(PREFERRED_GAMEPAD));
+    app.update();
+
+    // ... We should receive a Jump action!
+    let action_state = app.world.resource_mut::<ActionState<MyAction>>();
+    assert!(action_state.pressed(&MyAction::Jump));
+}
+
+#[test]
+fn filters_out_other_gamepads() {
+    let mut app = create_test_app();
+
+    const PREFERRED_GAMEPAD: Gamepad = Gamepad { id: 2 };
+    const OTHER_GAMEPAD: Gamepad = Gamepad { id: 1 };
+
+    let mut input_map = InputMap::new([(MyAction::Jump, GamepadButtonType::South)]);
+    input_map.set_gamepad(PREFERRED_GAMEPAD);
+    app.insert_resource(input_map);
+    app.init_resource::<ActionState<MyAction>>();
+
+    // When we press the Jump button...
+    let mut events = app.world.resource_mut::<Events<GamepadEvent>>();
+    events.send(jump_button_press_event(OTHER_GAMEPAD));
+    app.update();
+
+    // ... We should receive a Jump action!
+    let action_state = app.world.resource_mut::<ActionState<MyAction>>();
+    assert!(action_state.released(&MyAction::Jump));
+}