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

Issue:
Generate action diffs for only a filtered set of entities
## What problem does this solve?

In a networked game, there may be cases where actions on some entities should remain local, while actions on others should get diffed and have `ActionDiffEvent<A>`s generated.

Currently, `SummarizedActionState<A>::summarize` & `generate_action_diffs<A>` summarize and emit action diffs for every entity with `ActionState<A>`, which lacks the degree of control I'm looking for.

## What solution would you like?

I'd like to be able to filter which entities get action diffs generated.

Since we need to add the `generate_action_diffs<A>` system manually anyway, this seems to be the best place to specify it, e.g. `generate_action_diffs<A, Without<NotActionSender<A>>` (where `NotActionSender<A>` is an example of a tag component that marks anything for which action diff events should not be sent).

## How could this be implemented?

Implement a new `generate_action_diffs_filtered<A, F = QueryFilter>` function, and have `generate_action_diffs<A>` call it with `F = ()`. User code can choose between adding the `generate_action_diffs<A>` or `generate_action_diffs_filtered<A, F>` systems according to their needs.

See PR #601.

## What alternatives have you considered?

- Require user code to use different Actionlikes to differentiate actions that should be diffed and those that shouldn't, and only call `generate_action_diffs` on the former. It's a pretty big burden on the user side though.
- Create a new tag component in this crate (e.g. `NotActionSender<A>`, or a better name) that users can tag entities with, and have `generate_action_diffs<A>` always ignore entities with this tag. A bit less flexible.

Pull request:
Add ability to filter entities for which action diffs should be generated
Fixes #600.

Patch:
diff --git a/RELEASES.md b/RELEASES.md
index 67ba0f8..90dcd93 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -9,6 +9,9 @@
   - removed `KeyboardVirtualDPad` and `GamepadVirtualDPad` in favor of `VirtualDPad`
   - removed `KeyboardVirtualDPad3D` in favor of `VirtualDPad3D`
 - added `threshold` value for `GamepadControlDirection`, `MouseMoveDirection`, and `MouseScrollDirection` to be considered pressed.
+- added ability to filter entities to generate action diffs for:
+  - added new `generate_action_diffs_filtered<A, F>` system, which accepts a `QueryFilter`, so that only entities matching QueryFilter `F` (and with `ActionState<A>`) generate action diffs
+  - added new `summarize_filtered<F>` function for `SummarizedActionState<A>` (alongside the original `summarize`).
 
 ## Version 0.15.1
 
diff --git a/src/action_diff.rs b/src/action_diff.rs
index d1a8ef6..ae990aa 100644
--- a/src/action_diff.rs
+++ b/src/action_diff.rs
@@ -9,6 +9,7 @@ use bevy::{
     ecs::{
         entity::{Entity, MapEntities},
         event::Event,
+        query::QueryFilter,
     },
     math::{Vec2, Vec3},
     prelude::{EntityMapper, EventWriter, Query, Res},
@@ -112,10 +113,19 @@ impl<A: Actionlike> SummarizedActionState<A> {
         entities
     }
 
-    /// Captures the raw values for each action in the current frame.
+    /// Captures the raw values for each action in the current frame, for all entities with `ActionState<A>`.
     pub fn summarize(
         global_action_state: Option<Res<ActionState<A>>>,
         action_state_query: Query<(Entity, &ActionState<A>)>,
+    ) -> Self {
+        Self::summarize_filtered(global_action_state, action_state_query)
+    }
+
+    /// Captures the raw values for each action in the current frame, for entities with `ActionState<A>`
+    /// matching the query filter.
+    pub fn summarize_filtered<F: QueryFilter>(
+        global_action_state: Option<Res<ActionState<A>>>,
+        action_state_query: Query<(Entity, &ActionState<A>), F>,
     ) -> Self {
         let mut button_state_map = HashMap::default();
         let mut axis_state_map = HashMap::default();
@@ -389,6 +399,9 @@ mod tests {
         action_state
     }
 
+    #[derive(Component)]
+    struct NotSummarized;
+
     fn expected_summary(entity: Entity) -> SummarizedActionState<TestAction> {
         let mut button_state_map = HashMap::default();
         let mut axis_state_map = HashMap::default();
@@ -449,6 +462,25 @@ mod tests {
         assert_eq!(summarized, expected_summary(entity));
     }
 
+    #[test]
+    fn summarize_filtered_entities_from_component() {
+        // Spawn two entities, one to be summarized and one to be filtered out
+        let mut world = World::new();
+        let entity = world.spawn(test_action_state()).id();
+        world.spawn((test_action_state(), NotSummarized));
+
+        let mut system_state: SystemState<(
+            Option<Res<ActionState<TestAction>>>,
+            Query<(Entity, &ActionState<TestAction>), Without<NotSummarized>>,
+        )> = SystemState::new(&mut world);
+        let (global_action_state, action_state_query) = system_state.get(&world);
+        let summarized =
+            SummarizedActionState::summarize_filtered(global_action_state, action_state_query);
+
+        // Check that only the entity without NotSummarized was summarized
+        assert_eq!(summarized, expected_summary(entity));
+    }
+
     #[test]
     fn diffs_are_sent() {
         let mut world = World::new();
diff --git a/src/systems.rs b/src/systems.rs
index 7bc69ba..6154684 100644
--- a/src/systems.rs
+++ b/src/systems.rs
@@ -3,8 +3,10 @@
 use crate::prelude::updating::CentralInputStore;
 #[cfg(feature = "mouse")]
 use crate::user_input::{AccumulatedMouseMovement, AccumulatedMouseScroll};
+use bevy::ecs::query::QueryFilter;
 #[cfg(feature = "mouse")]
 use bevy::input::mouse::{MouseMotion, MouseWheel};
+use bevy::log::debug;
 
 use crate::{
     action_state::ActionState, clashing_inputs::ClashStrategy, input_map::InputMap, Actionlike,
@@ -169,13 +171,35 @@ pub fn filter_captured_input(
 pub fn generate_action_diffs<A: Actionlike>(
     global_action_state: Option<Res<ActionState<A>>>,
     action_state_query: Query<(Entity, &ActionState<A>)>,
+    previous_action_state: Local<SummarizedActionState<A>>,
+    action_diff_events: EventWriter<ActionDiffEvent<A>>,
+) {
+    generate_action_diffs_filtered(
+        global_action_state,
+        action_state_query,
+        previous_action_state,
+        action_diff_events,
+    )
+}
+
+/// Generates an [`Events`] stream of [`ActionDiff`s](crate::action_diff::ActionDiff) from the [`ActionState`] of certain entities.
+///
+/// This system is not part of the [`InputManagerPlugin`](crate::plugin::InputManagerPlugin) and must be added manually.
+/// Generally speaking, this should be added as part of [`PostUpdate`](bevy::prelude::PostUpdate),
+/// to ensure that all inputs have been processed and any manual actions have been sent.
+///
+/// This system accepts a [`QueryFilter`] to limit which entities should have action diffs generated.
+pub fn generate_action_diffs_filtered<A: Actionlike, F: QueryFilter>(
+    global_action_state: Option<Res<ActionState<A>>>,
+    action_state_query: Query<(Entity, &ActionState<A>), F>,
     mut previous_action_state: Local<SummarizedActionState<A>>,
     mut action_diff_events: EventWriter<ActionDiffEvent<A>>,
 ) {
     let current_action_state =
-        SummarizedActionState::summarize(global_action_state, action_state_query);
+        SummarizedActionState::summarize_filtered(global_action_state, action_state_query);
     current_action_state.send_diffs(&previous_action_state, &mut action_diff_events);
-    dbg!(&previous_action_state, &current_action_state);
+    debug!("previous_action_state: {:?}", previous_action_state);
+    debug!("current_action_state: {:?}", current_action_state);
     *previous_action_state = current_action_state;
 }
 


Test patch:
diff --git a/tests/action_diffs.rs b/tests/action_diffs.rs
index f2b1812..d8e91f0 100644
--- a/tests/action_diffs.rs
+++ b/tests/action_diffs.rs
@@ -1,6 +1,9 @@
 use bevy::{input::InputPlugin, prelude::*};
 use leafwing_input_manager::action_diff::{ActionDiff, ActionDiffEvent};
-use leafwing_input_manager::{prelude::*, systems::generate_action_diffs};
+use leafwing_input_manager::{
+    prelude::*,
+    systems::{generate_action_diffs, generate_action_diffs_filtered},
+};
 
 #[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
 enum Action {
@@ -11,6 +14,9 @@ enum Action {
     DualAxis,
 }
 
+#[derive(Component)]
+pub struct NoActionDiffs;
+
 fn spawn_test_entity(mut commands: Commands) {
     commands.spawn(ActionState::<Action>::default());
 }
@@ -265,6 +271,56 @@ fn generate_axis_action_diffs() {
     });
 }
 
+#[test]
+fn generate_filtered_binary_action_diffs() {
+    let mut app = create_app();
+    app.add_systems(
+        PostUpdate,
+        generate_action_diffs_filtered::<Action, Without<NoActionDiffs>>,
+    );
+
+    let entity = app
+        .world_mut()
+        .query_filtered::<Entity, With<ActionState<Action>>>()
+        .single(app.world());
+    // Also spawn an entity that will not send action diffs
+    app.world_mut()
+        .spawn((ActionState::<Action>::default(), NoActionDiffs));
+
+    // Press both entities
+    for mut action_state in app
+        .world_mut()
+        .query::<&mut ActionState<Action>>()
+        .iter_mut(app.world_mut())
+    {
+        action_state.press(&Action::Button);
+    }
+    app.update();
+
+    // Expect only `entity` to have an action diff event
+    assert_action_diff_created(&mut app, |action_diff_event| {
+        assert_eq!(action_diff_event.owner, Some(entity));
+        assert_eq!(action_diff_event.action_diffs.len(), 1);
+        match action_diff_event.action_diffs.first().unwrap().clone() {
+            ActionDiff::Pressed { action } => {
+                assert_eq!(action, Action::Button);
+            }
+            ActionDiff::Released { .. } => {
+                panic!("Expected a `Pressed` variant got a `Released` variant")
+            }
+            ActionDiff::AxisChanged { .. } => {
+                panic!("Expected a `Pressed` variant got a `ValueChanged` variant")
+            }
+            ActionDiff::DualAxisChanged { .. } => {
+                panic!("Expected a `Pressed` variant got a `AxisPairChanged` variant")
+            }
+            ActionDiff::TripleAxisChanged { .. } => {
+                panic!("Expected a `Pressed` variant got a `TripleAxisChanged` variant")
+            }
+        }
+    });
+}
+
 #[test]
 fn process_binary_action_diffs() {
     let mut app = create_app();