use std::cmp::Ordering;
use bevy::prelude::{Gamepad, Resource};
use serde::{Deserialize, Serialize};
use crate::input_map::{InputMap, UpdatedActions};
use crate::prelude::updating::CentralInputStore;
use crate::user_input::Buttonlike;
use crate::{Actionlike, InputControlKind};
#[non_exhaustive]
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default)]
pub enum ClashStrategy {
PressAll,
#[default]
PrioritizeLongest,
}
impl ClashStrategy {
pub fn variants() -> &'static [ClashStrategy] {
use ClashStrategy::*;
&[PressAll, PrioritizeLongest]
}
}
#[derive(Debug, Clone)]
#[must_use]
pub enum BasicInputs {
None,
Simple(Box<dyn Buttonlike>),
Composite(Vec<Box<dyn Buttonlike>>),
Chord(Vec<Box<dyn Buttonlike>>),
}
impl BasicInputs {
#[inline]
pub fn inputs(&self) -> Vec<Box<dyn Buttonlike>> {
match self.clone() {
Self::None => Vec::default(),
Self::Simple(input) => vec![input],
Self::Composite(inputs) => inputs,
Self::Chord(inputs) => inputs,
}
}
pub fn compose(self, other: BasicInputs) -> Self {
let combined_inputs = self.inputs().into_iter().chain(other.inputs()).collect();
BasicInputs::Composite(combined_inputs)
}
#[allow(clippy::len_without_is_empty)]
#[inline]
pub fn len(&self) -> usize {
match self {
Self::None => 0,
Self::Simple(_) => 1,
Self::Composite(_) => 1,
Self::Chord(inputs) => inputs.len(),
}
}
#[inline]
pub fn clashes_with(&self, other: &BasicInputs) -> bool {
match (self, other) {
(Self::None, _) | (_, Self::None) => false,
(Self::Simple(_), Self::Simple(_)) => false,
(Self::Simple(self_single), Self::Chord(other_group)) => {
other_group.len() > 1 && other_group.contains(self_single)
}
(Self::Chord(self_group), Self::Simple(other_single)) => {
self_group.len() > 1 && self_group.contains(other_single)
}
(Self::Simple(self_single), Self::Composite(other_composite)) => {
other_composite.contains(self_single)
}
(Self::Composite(self_composite), Self::Simple(other_single)) => {
self_composite.contains(other_single)
}
(Self::Composite(self_composite), Self::Chord(other_group)) => {
other_group.len() > 1
&& other_group
.iter()
.any(|input| self_composite.contains(input))
}
(Self::Chord(self_group), Self::Composite(other_composite)) => {
self_group.len() > 1
&& self_group
.iter()
.any(|input| other_composite.contains(input))
}
(Self::Chord(self_group), Self::Chord(other_group)) => {
self_group.len() > 1
&& other_group.len() > 1
&& self_group != other_group
&& (self_group.iter().all(|input| other_group.contains(input))
|| other_group.iter().all(|input| self_group.contains(input)))
}
(Self::Composite(self_composite), Self::Composite(other_composite)) => {
other_composite
.iter()
.any(|input| self_composite.contains(input))
|| self_composite
.iter()
.any(|input| other_composite.contains(input))
}
}
}
}
impl<A: Actionlike> InputMap<A> {
pub fn handle_clashes(
&self,
updated_actions: &mut UpdatedActions<A>,
input_store: &CentralInputStore,
clash_strategy: ClashStrategy,
gamepad: Gamepad,
) {
for clash in self.get_clashes(updated_actions, input_store, gamepad) {
if let Some(culled_action) = resolve_clash(&clash, clash_strategy, input_store, gamepad)
{
updated_actions.remove(&culled_action);
}
}
}
pub(crate) fn possible_clashes(&self) -> Vec<Clash<A>> {
let mut clashes = Vec::default();
for action_a in self.buttonlike_actions() {
for action_b in self.buttonlike_actions() {
if let Some(clash) = self.possible_clash(action_a, action_b) {
clashes.push(clash);
}
}
}
clashes
}
#[must_use]
fn get_clashes(
&self,
updated_actions: &UpdatedActions<A>,
input_store: &CentralInputStore,
gamepad: Gamepad,
) -> Vec<Clash<A>> {
let mut clashes = Vec::default();
for clash in self.possible_clashes() {
let pressed_a = updated_actions.pressed(&clash.action_a);
let pressed_b = updated_actions.pressed(&clash.action_b);
if pressed_a && pressed_b {
if let Some(clash) = check_clash(&clash, input_store, gamepad) {
clashes.push(clash)
}
}
}
clashes
}
pub fn decomposed(&self, action: &A) -> Vec<BasicInputs> {
match action.input_control_kind() {
InputControlKind::Button => {
let Some(buttonlike) = self.get_buttonlike(action) else {
return Vec::new();
};
buttonlike.iter().map(|input| input.decompose()).collect()
}
InputControlKind::Axis => {
let Some(axislike) = self.get_axislike(action) else {
return Vec::new();
};
axislike.iter().map(|input| input.decompose()).collect()
}
InputControlKind::DualAxis => {
let Some(dual_axislike) = self.get_dual_axislike(action) else {
return Vec::new();
};
dual_axislike
.iter()
.map(|input| input.decompose())
.collect()
}
InputControlKind::TripleAxis => {
let Some(triple_axislike) = self.get_triple_axislike(action) else {
return Vec::new();
};
triple_axislike
.iter()
.map(|input| input.decompose())
.collect()
}
}
}
#[must_use]
fn possible_clash(&self, action_a: &A, action_b: &A) -> Option<Clash<A>> {
let mut clash = Clash::new(action_a.clone(), action_b.clone());
for input_a in self.get_buttonlike(action_a)? {
for input_b in self.get_buttonlike(action_b)? {
if input_a.decompose().clashes_with(&input_b.decompose()) {
clash.inputs_a.push(input_a.clone());
clash.inputs_b.push(input_b.clone());
}
}
}
let clashed = !clash.inputs_a.is_empty();
clashed.then_some(clash)
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub(crate) struct Clash<A: Actionlike> {
action_a: A,
action_b: A,
inputs_a: Vec<Box<dyn Buttonlike>>,
inputs_b: Vec<Box<dyn Buttonlike>>,
}
impl<A: Actionlike> Clash<A> {
#[must_use]
fn new(action_a: A, action_b: A) -> Self {
Self {
action_a,
action_b,
inputs_a: Vec::default(),
inputs_b: Vec::default(),
}
}
}
#[must_use]
fn check_clash<A: Actionlike>(
clash: &Clash<A>,
input_store: &CentralInputStore,
gamepad: Gamepad,
) -> Option<Clash<A>> {
let mut actual_clash: Clash<A> = clash.clone();
for input_a in clash
.inputs_a
.iter()
.filter(|&input| input.pressed(input_store, gamepad))
{
for input_b in clash
.inputs_b
.iter()
.filter(|&input| input.pressed(input_store, gamepad))
{
if input_a.decompose().clashes_with(&input_b.decompose()) {
actual_clash.inputs_a.push(input_a.clone());
actual_clash.inputs_b.push(input_b.clone());
}
}
}
let clashed = !clash.inputs_a.is_empty();
clashed.then_some(actual_clash)
}
#[must_use]
fn resolve_clash<A: Actionlike>(
clash: &Clash<A>,
clash_strategy: ClashStrategy,
input_store: &CentralInputStore,
gamepad: Gamepad,
) -> Option<A> {
let reasons_a_is_pressed: Vec<&dyn Buttonlike> = clash
.inputs_a
.iter()
.filter(|input| input.pressed(input_store, gamepad))
.map(|input| input.as_ref())
.collect();
let reasons_b_is_pressed: Vec<&dyn Buttonlike> = clash
.inputs_b
.iter()
.filter(|input| input.pressed(input_store, gamepad))
.map(|input| input.as_ref())
.collect();
for reason_a in reasons_a_is_pressed.iter() {
for reason_b in reasons_b_is_pressed.iter() {
if !reason_a.decompose().clashes_with(&reason_b.decompose()) {
return None;
}
}
}
match clash_strategy {
ClashStrategy::PressAll => None,
ClashStrategy::PrioritizeLongest => {
let longest_a: usize = reasons_a_is_pressed
.iter()
.map(|input| input.decompose().len())
.reduce(|a, b| a.max(b))
.unwrap_or_default();
let longest_b: usize = reasons_b_is_pressed
.iter()
.map(|input| input.decompose().len())
.reduce(|a, b| a.max(b))
.unwrap_or_default();
match longest_a.cmp(&longest_b) {
Ordering::Greater => Some(clash.action_b.clone()),
Ordering::Less => Some(clash.action_a.clone()),
Ordering::Equal => None,
}
}
}
}
#[cfg(feature = "keyboard")]
#[cfg(test)]
mod tests {
use bevy::input::keyboard::KeyCode::*;
use bevy::prelude::Reflect;
use super::*;
use crate::prelude::{KeyboardVirtualDPad, UserInput};
use crate::user_input::ButtonlikeChord;
use crate as leafwing_input_manager;
#[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)]
enum Action {
One,
Two,
OneAndTwo,
TwoAndThree,
OneAndTwoAndThree,
CtrlOne,
AltOne,
CtrlAltOne,
CtrlUp,
#[actionlike(DualAxis)]
MoveDPad,
}
fn test_input_map() -> InputMap<Action> {
use Action::*;
let mut input_map = InputMap::default();
input_map.insert(One, Digit1);
input_map.insert(Two, Digit2);
input_map.insert(OneAndTwo, ButtonlikeChord::new([Digit1, Digit2]));
input_map.insert(TwoAndThree, ButtonlikeChord::new([Digit2, Digit3]));
input_map.insert(
OneAndTwoAndThree,
ButtonlikeChord::new([Digit1, Digit2, Digit3]),
);
input_map.insert(CtrlOne, ButtonlikeChord::new([ControlLeft, Digit1]));
input_map.insert(AltOne, ButtonlikeChord::new([AltLeft, Digit1]));
input_map.insert(
CtrlAltOne,
ButtonlikeChord::new([ControlLeft, AltLeft, Digit1]),
);
input_map.insert_dual_axis(MoveDPad, KeyboardVirtualDPad::ARROW_KEYS);
input_map.insert(CtrlUp, ButtonlikeChord::new([ControlLeft, ArrowUp]));
input_map
}
fn inputs_clash(input_a: impl UserInput, input_b: impl UserInput) -> bool {
let decomposed_a = input_a.decompose();
println!("{decomposed_a:?}");
let decomposed_b = input_b.decompose();
println!("{decomposed_b:?}");
let do_inputs_clash = decomposed_a.clashes_with(&decomposed_b);
println!("Clash: {do_inputs_clash}");
do_inputs_clash
}
mod basic_functionality {
use crate::{
input_map::UpdatedValue,
plugin::{AccumulatorPlugin, CentralInputStorePlugin},
prelude::{AccumulatedMouseMovement, AccumulatedMouseScroll, ModifierKey},
};
use bevy::{input::InputPlugin, prelude::*};
use Action::*;
use super::*;
#[test]
#[ignore = "Figuring out how to handle the length of chords with group inputs is out of scope."]
fn input_types_have_right_length() {
let simple = KeyA.decompose();
assert_eq!(simple.len(), 1);
let empty_chord = ButtonlikeChord::default().decompose();
assert_eq!(empty_chord.len(), 0);
let chord = ButtonlikeChord::new([KeyA, KeyB, KeyC]).decompose();
assert_eq!(chord.len(), 3);
let modifier = ModifierKey::Control.decompose();
assert_eq!(modifier.len(), 1);
let modified_chord = ButtonlikeChord::modified(ModifierKey::Control, KeyA).decompose();
assert_eq!(modified_chord.len(), 2);
let group = KeyboardVirtualDPad::WASD.decompose();
assert_eq!(group.len(), 1);
}
#[test]
fn clash_detection() {
let a = KeyA;
let b = KeyB;
let c = KeyC;
let ab = ButtonlikeChord::new([KeyA, KeyB]);
let bc = ButtonlikeChord::new([KeyB, KeyC]);
let abc = ButtonlikeChord::new([KeyA, KeyB, KeyC]);
let axyz_dpad = KeyboardVirtualDPad::new(KeyA, KeyX, KeyY, KeyZ);
let abcd_dpad = KeyboardVirtualDPad::WASD;
let ctrl_up = ButtonlikeChord::new([ArrowUp, ControlLeft]);
let directions_dpad = KeyboardVirtualDPad::ARROW_KEYS;
assert!(!inputs_clash(a, b));
assert!(inputs_clash(a, ab.clone()));
assert!(!inputs_clash(c, ab.clone()));
assert!(!inputs_clash(ab.clone(), bc.clone()));
assert!(inputs_clash(ab.clone(), abc.clone()));
assert!(inputs_clash(axyz_dpad.clone(), a));
assert!(inputs_clash(axyz_dpad.clone(), ab.clone()));
assert!(!inputs_clash(axyz_dpad.clone(), bc.clone()));
assert!(inputs_clash(axyz_dpad.clone(), abcd_dpad.clone()));
assert!(inputs_clash(ctrl_up.clone(), directions_dpad.clone()));
}
#[test]
fn button_chord_clash_construction() {
let input_map = test_input_map();
let observed_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap();
let correct_clash = Clash {
action_a: One,
action_b: OneAndTwo,
inputs_a: vec![Box::new(Digit1)],
inputs_b: vec![Box::new(ButtonlikeChord::new([Digit1, Digit2]))],
};
assert_eq!(observed_clash, correct_clash);
}
#[test]
fn chord_chord_clash_construction() {
let input_map = test_input_map();
let observed_clash = input_map
.possible_clash(&OneAndTwoAndThree, &OneAndTwo)
.unwrap();
let correct_clash = Clash {
action_a: OneAndTwoAndThree,
action_b: OneAndTwo,
inputs_a: vec![Box::new(ButtonlikeChord::new([Digit1, Digit2, Digit3]))],
inputs_b: vec![Box::new(ButtonlikeChord::new([Digit1, Digit2]))],
};
assert_eq!(observed_clash, correct_clash);
}
#[test]
fn can_clash() {
let input_map = test_input_map();
assert!(input_map.possible_clash(&One, &Two).is_none());
assert!(input_map.possible_clash(&One, &OneAndTwo).is_some());
assert!(input_map.possible_clash(&One, &OneAndTwoAndThree).is_some());
assert!(input_map.possible_clash(&One, &TwoAndThree).is_none());
assert!(input_map
.possible_clash(&OneAndTwo, &OneAndTwoAndThree)
.is_some());
}
#[test]
fn resolve_prioritize_longest() {
let mut app = App::new();
app.add_plugins((InputPlugin, AccumulatorPlugin, CentralInputStorePlugin));
app.init_resource::<AccumulatedMouseMovement>();
app.init_resource::<AccumulatedMouseScroll>();
let input_map = test_input_map();
let simple_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap();
Digit1.press(app.world_mut());
Digit2.press(app.world_mut());
app.update();
let input_store = app.world().resource::<CentralInputStore>();
assert_eq!(
resolve_clash(
&simple_clash,
ClashStrategy::PrioritizeLongest,
input_store,
Gamepad::new(0),
),
Some(One)
);
let reversed_clash = input_map.possible_clash(&OneAndTwo, &One).unwrap();
let input_store = app.world().resource::<CentralInputStore>();
assert_eq!(
resolve_clash(
&reversed_clash,
ClashStrategy::PrioritizeLongest,
input_store,
Gamepad::new(0),
),
Some(One)
);
let chord_clash = input_map
.possible_clash(&OneAndTwo, &OneAndTwoAndThree)
.unwrap();
Digit3.press(app.world_mut());
app.update();
let input_store = app.world().resource::<CentralInputStore>();
assert_eq!(
resolve_clash(
&chord_clash,
ClashStrategy::PrioritizeLongest,
input_store,
Gamepad::new(0),
),
Some(OneAndTwo)
);
}
#[test]
fn handle_simple_clash() {
let mut app = App::new();
app.add_plugins((InputPlugin, AccumulatorPlugin, CentralInputStorePlugin));
let input_map = test_input_map();
Digit1.press(app.world_mut());
Digit2.press(app.world_mut());
app.update();
let mut updated_actions = UpdatedActions::default();
updated_actions.insert(One, UpdatedValue::Button(true));
updated_actions.insert(Two, UpdatedValue::Button(true));
updated_actions.insert(OneAndTwo, UpdatedValue::Button(true));
let input_store = app.world().resource::<CentralInputStore>();
input_map.handle_clashes(
&mut updated_actions,
input_store,
ClashStrategy::PrioritizeLongest,
Gamepad::new(0),
);
let mut expected = UpdatedActions::default();
expected.insert(OneAndTwo, UpdatedValue::Button(true));
assert_eq!(updated_actions, expected);
}
#[test]
#[ignore = "Clashing inputs for non-buttonlike inputs is broken."]
fn handle_clashes_dpad_chord() {
let mut app = App::new();
app.add_plugins(InputPlugin);
app.init_resource::<AccumulatedMouseMovement>();
app.init_resource::<AccumulatedMouseScroll>();
let input_map = test_input_map();
ControlLeft.press(app.world_mut());
ArrowUp.press(app.world_mut());
app.update();
let mut updated_actions = UpdatedActions::default();
updated_actions.insert(CtrlUp, UpdatedValue::Button(true));
updated_actions.insert(MoveDPad, UpdatedValue::Button(true));
let chord_input = input_map.get_buttonlike(&CtrlUp).unwrap().first().unwrap();
let dpad_input = input_map
.get_dual_axislike(&MoveDPad)
.unwrap()
.first()
.unwrap();
assert!(chord_input
.decompose()
.clashes_with(&dpad_input.decompose()));
input_map
.possible_clash(&CtrlUp, &MoveDPad)
.expect("Clash not detected");
assert!(chord_input.decompose().len() > dpad_input.decompose().len());
let input_store = app.world().resource::<CentralInputStore>();
input_map.handle_clashes(
&mut updated_actions,
input_store,
ClashStrategy::PrioritizeLongest,
Gamepad::new(0),
);
let mut expected = UpdatedActions::default();
expected.insert(CtrlUp, UpdatedValue::Button(true));
assert_eq!(updated_actions, expected);
}
#[test]
fn check_which_pressed() {
let mut app = App::new();
app.add_plugins((InputPlugin, AccumulatorPlugin, CentralInputStorePlugin));
app.init_resource::<AccumulatedMouseMovement>();
app.init_resource::<AccumulatedMouseScroll>();
let input_map = test_input_map();
Digit1.press(app.world_mut());
Digit2.press(app.world_mut());
ControlLeft.press(app.world_mut());
app.update();
let input_store = app.world().resource::<CentralInputStore>();
let action_data = input_map.process_actions(
&Gamepads::default(),
input_store,
ClashStrategy::PrioritizeLongest,
);
for (action, &updated_value) in action_data.iter() {
if *action == CtrlOne || *action == OneAndTwo {
assert_eq!(updated_value, UpdatedValue::Button(true));
} else {
match updated_value {
UpdatedValue::Button(pressed) => assert!(!pressed),
UpdatedValue::Axis(value) => assert_eq!(value, 0.0),
UpdatedValue::DualAxis(pair) => assert_eq!(pair, Vec2::ZERO),
UpdatedValue::TripleAxis(triple) => assert_eq!(triple, Vec3::ZERO),
}
}
}
}
}
}