diff --git a/RELEASES.md b/RELEASES.md index 861edd7a..c42ef299 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,6 +11,7 @@ ### Bugs - fixed compilation issues with no-default-features +- fixed [a bug](https://github.com/Leafwing-Studios/leafwing-input-manager/issues/471) related to incorrect updating of `ActionState`. ## Version 0.12 diff --git a/src/action_state.rs b/src/action_state.rs index 5136eb09..3c09a70f 100644 --- a/src/action_state.rs +++ b/src/action_state.rs @@ -103,6 +103,11 @@ impl ActionState { /// The `action_data` is typically constructed from [`InputMap::which_pressed`](crate::input_map::InputMap), /// which reads from the assorted [`Input`](bevy::input::Input) resources. pub fn update(&mut self, action_data: HashMap) { + for (action, action_datum) in self.action_data.iter_mut() { + if !action_data.contains_key(action) { + action_datum.state.release(); + } + } for (action, action_datum) in action_data { match self.action_data.entry(action) { Entry::Occupied(occupied_entry) => { @@ -585,30 +590,28 @@ impl ActionState { #[cfg(test)] mod tests { use crate as leafwing_input_manager; + use crate::action_state::ActionState; + use crate::clashing_inputs::ClashStrategy; + use crate::input_map::InputMap; use crate::input_mocking::MockInput; - use bevy::prelude::Reflect; + use crate::input_streams::InputStreams; + use bevy::input::InputPlugin; + use bevy::prelude::*; + use bevy::utils::{Duration, Instant}; use leafwing_input_manager_macros::Actionlike; - #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)] - enum Action { - Run, - Jump, - Hide, - } - #[test] fn press_lifecycle() { - use crate::action_state::ActionState; - use crate::clashing_inputs::ClashStrategy; - use crate::input_map::InputMap; - use crate::input_streams::InputStreams; - use bevy::input::InputPlugin; - use bevy::prelude::*; - use bevy::utils::{Duration, Instant}; - let mut app = App::new(); app.add_plugins(InputPlugin); + #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, bevy::prelude::Reflect)] + enum Action { + Run, + Jump, + Hide, + } + // Action state let mut action_state = ActionState::::default(); @@ -668,4 +671,79 @@ mod tests { assert!(action_state.released(&Action::Run)); assert!(!action_state.just_released(&Action::Run)); } + + #[test] + fn update_with_clashes_prioritizing_longest() { + #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)] + enum Action { + One, + Two, + OneAndTwo, + } + + // Input map + use bevy::prelude::KeyCode::*; + let mut input_map = InputMap::default(); + input_map.insert(Action::One, Key1); + input_map.insert(Action::Two, Key2); + input_map.insert_chord(Action::OneAndTwo, [Key1, Key2]); + + let mut app = App::new(); + app.add_plugins(InputPlugin); + + // Action state + let mut action_state = ActionState::::default(); + + // Starting state + let input_streams = InputStreams::from_world(&app.world, None); + action_state + .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + assert!(action_state.released(&Action::One)); + assert!(action_state.released(&Action::Two)); + assert!(action_state.released(&Action::OneAndTwo)); + + // Pressing One + app.send_input(Key1); + app.update(); + let input_streams = InputStreams::from_world(&app.world, None); + + action_state + .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + + assert!(action_state.pressed(&Action::One)); + assert!(action_state.released(&Action::Two)); + assert!(action_state.released(&Action::OneAndTwo)); + + // Waiting + action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); + action_state + .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + + assert!(action_state.pressed(&Action::One)); + assert!(action_state.released(&Action::Two)); + assert!(action_state.released(&Action::OneAndTwo)); + + // Pressing Two + app.send_input(Key2); + app.update(); + let input_streams = InputStreams::from_world(&app.world, None); + + action_state + .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + + // Now only the longest OneAndTwo has been pressed, + // while both One and Two have been released + assert!(action_state.released(&Action::One)); + assert!(action_state.released(&Action::Two)); + assert!(action_state.pressed(&Action::OneAndTwo)); + + // Waiting + action_state.tick(Instant::now(), Instant::now() - Duration::from_micros(1)); + action_state + .update(input_map.which_pressed(&input_streams, ClashStrategy::PrioritizeLongest)); + + assert!(action_state.released(&Action::One)); + assert!(action_state.released(&Action::Two)); + assert!(action_state.pressed(&Action::OneAndTwo)); + } }