use std::sync::atomic::{AtomicU32, Ordering};
use super::{ActionMap, Binding, Device, Pads, TracedInput};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GamepadId(pub u32);
impl GamepadId {
#[must_use]
pub fn allocate() -> Self {
static NEXT: AtomicU32 = AtomicU32::new(1);
let id = NEXT.fetch_add(1, Ordering::Relaxed);
assert_ne!(id, 0, "gamepad ids wrapped");
Self(id)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PadButton {
South,
East,
West,
North,
LeftShoulder,
RightShoulder,
LeftStick,
RightStick,
Start,
Select,
DpadUp,
DpadDown,
DpadLeft,
DpadRight,
Guide,
}
impl PadButton {
pub const ALL: [Self; 15] = [
Self::South,
Self::East,
Self::West,
Self::North,
Self::LeftShoulder,
Self::RightShoulder,
Self::LeftStick,
Self::RightStick,
Self::Start,
Self::Select,
Self::DpadUp,
Self::DpadDown,
Self::DpadLeft,
Self::DpadRight,
Self::Guide,
];
pub(crate) const DPAD: [Self; 4] = [
Self::DpadUp,
Self::DpadDown,
Self::DpadLeft,
Self::DpadRight,
];
const fn bit(self) -> u16 {
1 << self as u16
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct PadButtons(u16);
impl PadButtons {
pub const EMPTY: Self = Self(0);
#[must_use]
pub const fn contains(self, button: PadButton) -> bool {
self.0 & button.bit() != 0
}
pub const fn insert(&mut self, button: PadButton) {
self.0 |= button.bit();
}
pub const fn remove(&mut self, button: PadButton) {
self.0 &= !button.bit();
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub(crate) const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
const fn intersection(self, other: Self) -> Self {
Self(self.0 & other.0)
}
const fn difference(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
}
impl FromIterator<PadButton> for PadButtons {
fn from_iter<I: IntoIterator<Item = PadButton>>(buttons: I) -> Self {
let mut set = Self::EMPTY;
for button in buttons {
set.insert(button);
}
set
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PadAxis {
LeftX,
LeftY,
RightX,
RightY,
LeftTrigger,
RightTrigger,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Stick {
Left,
Right,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Trigger {
Left,
Right,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum PadKind {
Xbox,
PlayStation,
Switch,
SteamDeck,
#[default]
Generic,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GamepadSnapshot {
pub buttons: PadButtons,
pub axes: [f32; 6],
pub kind: PadKind,
}
impl GamepadSnapshot {
#[must_use]
pub const fn neutral(kind: PadKind) -> Self {
Self {
buttons: PadButtons::EMPTY,
axes: [0.0; 6],
kind,
}
}
#[must_use]
pub const fn axis(&self, axis: PadAxis) -> f32 {
self.axes[axis as usize]
}
#[must_use]
pub const fn stick(&self, stick: Stick) -> (f32, f32) {
match stick {
Stick::Left => (self.axis(PadAxis::LeftX), self.axis(PadAxis::LeftY)),
Stick::Right => (self.axis(PadAxis::RightX), self.axis(PadAxis::RightY)),
}
}
#[must_use]
pub const fn trigger(&self, trigger: Trigger) -> f32 {
match trigger {
Trigger::Left => self.axis(PadAxis::LeftTrigger),
Trigger::Right => self.axis(PadAxis::RightTrigger),
}
}
#[must_use]
pub fn is_finite(&self) -> bool {
self.axes.iter().all(|value| value.is_finite())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GamepadEvent {
Connected {
id: GamepadId,
kind: PadKind,
},
Disconnected {
id: GamepadId,
},
State {
id: GamepadId,
snapshot: GamepadSnapshot,
},
}
pub const PAD_ACTIVITY_THRESHOLD: f32 = 0.5;
#[must_use]
pub fn radial_deadzone((x, y): (f32, f32), deadzone: f32) -> (f32, f32) {
let magnitude = x.hypot(y);
if !magnitude.is_finite() || magnitude <= deadzone {
return (0.0, 0.0);
}
let scaled = (magnitude.min(1.0) - deadzone) / (1.0 - deadzone);
(x / magnitude * scaled, y / magnitude * scaled)
}
pub(crate) fn trigger_past(value: f32, threshold: f32) -> f32 {
let value = value.clamp(0.0, 1.0);
if value <= threshold {
0.0
} else {
(value - threshold) / (1.0 - threshold)
}
}
pub(crate) fn valid_deadzone(value: f32) -> bool {
(0.0..1.0).contains(&value)
}
const DEFLECTED_INPUTS: [TracedInput; 4] = [
TracedInput::PadStick(Stick::Left),
TracedInput::PadStick(Stick::Right),
TracedInput::PadTrigger(Trigger::Left),
TracedInput::PadTrigger(Trigger::Right),
];
fn deflected(snapshot: &GamepadSnapshot) -> [bool; 4] {
let stick = |stick| {
let (x, y) = snapshot.stick(stick);
x.hypot(y) > PAD_ACTIVITY_THRESHOLD
};
[
stick(Stick::Left),
stick(Stick::Right),
snapshot.trigger(Trigger::Left) > PAD_ACTIVITY_THRESHOLD,
snapshot.trigger(Trigger::Right) > PAD_ACTIVITY_THRESHOLD,
]
}
impl ActionMap {
pub fn gamepad_event(&mut self, event: &GamepadEvent) {
let mut pressed = PadButtons::EMPTY;
let mut went_over = [false; 4];
match *event {
GamepadEvent::Connected { id, kind } => {
self.pads.insert(id, GamepadSnapshot::neutral(kind));
}
GamepadEvent::Disconnected { id } => {
self.pads.remove(&id);
}
GamepadEvent::State { id, snapshot } => {
if !snapshot.is_finite() {
return;
}
let before = self
.pads
.insert(id, snapshot)
.unwrap_or(GamepadSnapshot::neutral(snapshot.kind));
pressed = snapshot.buttons.difference(before.buttons);
for ((over, was), is) in went_over
.iter_mut()
.zip(deflected(&before))
.zip(deflected(&snapshot))
{
*over = is && !was;
}
if !pressed.is_empty() || went_over.contains(&true) {
self.spoke(Device::Gamepad);
self.last_pad_kind = Some(snapshot.kind);
}
}
}
self.repad();
for button in PadButton::ALL {
if pressed.contains(button) {
self.traced(|| TracedInput::PadButton(button));
}
}
for (over, input) in went_over.into_iter().zip(DEFLECTED_INPUTS) {
if over {
self.traced(|| input);
}
}
}
pub fn release_gamepads(&mut self) {
self.suppressed.pad_buttons = self.suppressed.pad_buttons.union(self.held_pad_buttons);
for snapshot in self.pads.values_mut() {
*snapshot = GamepadSnapshot::neutral(snapshot.kind);
}
self.held_pad_buttons = PadButtons::EMPTY;
self.resolve_matching(Binding::reads_gamepad);
}
pub fn gamepads(&self) -> impl Iterator<Item = (GamepadId, &GamepadSnapshot)> {
self.pads.iter().map(|(&id, snapshot)| (id, snapshot))
}
fn repad(&mut self) {
let before = self.held_pad_buttons;
self.held_pad_buttons = self
.pads
.values()
.fold(PadButtons::EMPTY, |held, pad| held.union(pad.buttons));
let held = self.held_pad_buttons;
for button in PadButton::ALL {
if before.contains(button)
&& held.contains(button)
&& self.routes.pad_reader(button, before) != self.routes.pad_reader(button, held)
{
self.suppressed.pad_buttons.insert(button);
}
}
self.suppressed.pad_buttons = self
.suppressed
.pad_buttons
.intersection(self.held_pad_buttons);
for stick in [Stick::Left, Stick::Right] {
if self.stick_rests(stick) {
self.suppressed.pad_sticks.remove(&stick);
}
}
for trigger in [Trigger::Left, Trigger::Right] {
if self.trigger_rests(trigger) {
self.suppressed.pad_triggers.remove(&trigger);
}
}
self.resolve_matching(Binding::reads_gamepad);
}
pub(crate) fn stick_rests(&self, stick: Stick) -> bool {
let deadzone = self
.bindings_all()
.filter_map(|binding| match *binding {
Binding::PadStick {
stick: bound,
deadzone,
} if bound == stick => Some(deadzone),
_ => None,
})
.reduce(f32::min)
.unwrap_or(0.0);
self.pads.values().all(|pad| {
let (x, y) = pad.stick(stick);
x.hypot(y) <= deadzone
})
}
pub(crate) fn trigger_rests(&self, trigger: Trigger) -> bool {
let threshold = self
.bindings_all()
.filter_map(|binding| match *binding {
Binding::PadTrigger {
trigger: bound,
threshold,
} if bound == trigger => Some(threshold),
_ => None,
})
.reduce(f32::min)
.unwrap_or(0.0);
self.pads
.values()
.all(|pad| pad.trigger(trigger) <= threshold)
}
fn bindings_all(&self) -> impl Iterator<Item = &Binding> {
self.slots.iter().flat_map(|slot| slot.decl.bindings.iter())
}
}
pub(crate) fn pad_trigger(pads: &Pads, trigger: Trigger) -> f32 {
pads.values()
.map(|pad| pad.trigger(trigger))
.fold(0.0, f32::max)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ActionDecl, ActionKind, ActionMapError};
use crcbl_core::input::KeyCode;
const TICK: f32 = 1.0 / 60.0;
const PAD: GamepadId = GamepadId(7);
fn action(name: &str, kind: ActionKind, bindings: Vec<Binding>) -> ActionDecl {
ActionDecl {
name: name.to_owned(),
kind,
bindings,
}
}
fn state(id: GamepadId, snapshot: GamepadSnapshot) -> GamepadEvent {
GamepadEvent::State { id, snapshot }
}
fn holding(buttons: &[PadButton]) -> GamepadSnapshot {
GamepadSnapshot {
buttons: buttons.iter().copied().collect(),
..GamepadSnapshot::neutral(PadKind::Xbox)
}
}
fn with_axis(mut snapshot: GamepadSnapshot, axis: PadAxis, value: f32) -> GamepadSnapshot {
snapshot.axes[axis as usize] = value;
snapshot
}
fn jump_map() -> ActionMap {
let mut map = ActionMap::new();
map.declare(action(
"jump",
ActionKind::Button,
vec![Binding::PadButton(PadButton::South)],
));
map.gamepad_event(&GamepadEvent::Connected {
id: PAD,
kind: PadKind::Xbox,
});
map
}
fn close(actual: (f32, f32), expected: (f32, f32)) -> bool {
(actual.0 - expected.0).abs() < 1e-6 && (actual.1 - expected.1).abs() < 1e-6
}
#[test]
fn a_pad_button_presses_holds_and_releases_through_its_binding() {
let mut map = jump_map();
map.begin_tick(TICK);
map.gamepad_event(&state(PAD, holding(&[PadButton::East])));
assert!(!map.button_held("jump"), "East is not South");
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
assert!(map.just_pressed("jump"));
assert!(map.button_held("jump"));
map.begin_tick(TICK);
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
assert!(!map.just_pressed("jump"), "a re-sent level is not a press");
assert!(map.button_held("jump"));
map.begin_tick(TICK);
map.gamepad_event(&state(PAD, holding(&[])));
assert!(map.just_released("jump"));
assert!(!map.button_held("jump"));
}
#[test]
fn a_button_held_on_two_pads_is_released_by_the_last_to_let_go() {
let mut map = jump_map();
let other = GamepadId(8);
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
map.gamepad_event(&state(other, holding(&[PadButton::South])));
map.gamepad_event(&state(PAD, holding(&[])));
assert!(map.button_held("jump"), "the other pad still holds it");
map.gamepad_event(&state(other, holding(&[])));
assert!(!map.button_held("jump"));
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
map.gamepad_event(&state(other, holding(&[PadButton::South])));
map.gamepad_event(&state(other, holding(&[])));
assert!(map.button_held("jump"), "the first pad still holds it");
map.gamepad_event(&state(PAD, holding(&[])));
assert!(!map.button_held("jump"));
}
#[test]
fn the_radial_deadzone_zeroes_inside_rescales_outside_and_clamps() {
assert_eq!(radial_deadzone((0.1, 0.0), 0.2), (0.0, 0.0));
assert_eq!(radial_deadzone((0.0, -0.2), 0.2), (0.0, 0.0), "on the edge");
assert!(close(radial_deadzone((0.6, 0.0), 0.2), (0.5, 0.0)));
assert!(close(radial_deadzone((0.0, -1.0), 0.2), (0.0, -1.0)));
assert!(close(radial_deadzone((0.6, 0.8), 0.2), (0.6, 0.8)));
assert!(close(radial_deadzone((0.3, 0.4), 0.2), (0.225, 0.3)));
assert!(close(radial_deadzone((3.0, 4.0), 0.2), (0.6, 0.8)));
assert!(
close(radial_deadzone((0.3, 0.4), 0.0), (0.3, 0.4)),
"no zone"
);
}
#[test]
fn the_trigger_threshold_zeroes_below_and_rescales_above() {
assert_eq!(trigger_past(0.0, 0.25), 0.0);
assert_eq!(trigger_past(0.25, 0.25), 0.0, "at the threshold");
assert!((trigger_past(0.5, 0.25) - 1.0 / 3.0).abs() < 1e-6);
assert_eq!(trigger_past(1.0, 0.25), 1.0);
assert_eq!(trigger_past(2.0, 0.25), 1.0, "clamped to full pull");
}
#[test]
fn stick_and_trigger_bindings_resolve_through_their_deadzones() {
let mut map = ActionMap::new();
map.declare(action(
"move",
ActionKind::Axis2,
vec![Binding::PadStick {
stick: Stick::Left,
deadzone: 0.2,
}],
));
map.declare(action(
"throttle",
ActionKind::Axis1,
vec![Binding::PadTrigger {
trigger: Trigger::Right,
threshold: 0.25,
}],
));
map.declare(action(
"fire",
ActionKind::Button,
vec![Binding::PadTrigger {
trigger: Trigger::Right,
threshold: 0.25,
}],
));
let rest = GamepadSnapshot::neutral(PadKind::Xbox);
let drift = with_axis(rest, PadAxis::LeftX, 0.15);
map.gamepad_event(&state(PAD, drift));
assert_eq!(map.axis2("move"), (0.0, 0.0), "drift inside the zone");
let pushed = with_axis(rest, PadAxis::LeftX, 0.6);
let pulled = with_axis(pushed, PadAxis::RightTrigger, 0.5);
map.gamepad_event(&state(PAD, pulled));
assert!(
close(map.axis2("move"), (0.5, 0.0)),
"{:?}",
map.axis2("move")
);
assert!((map.axis1("throttle") - 1.0 / 3.0).abs() < 1e-6);
assert!(map.just_pressed("fire"), "past the threshold is down");
map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::RightTrigger, 0.25)));
assert!(map.just_released("fire"), "at the threshold is up");
assert_eq!(map.axis1("throttle"), 0.0);
}
#[test]
fn a_stick_pushed_up_reads_positive_y_like_the_up_key() {
let mut map = ActionMap::new();
map.declare(action(
"move",
ActionKind::Axis2,
vec![
Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
},
Binding::PadStick {
stick: Stick::Left,
deadzone: 0.0,
},
],
));
let rest = GamepadSnapshot::neutral(PadKind::Xbox);
map.key_event(KeyCode::KeyW, true);
let by_key = map.axis2("move");
map.key_event(KeyCode::KeyW, false);
map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::LeftY, 1.0)));
assert_eq!(map.axis2("move"), by_key, "stick up is W");
assert_eq!(map.axis2("move"), (0.0, 1.0));
map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::LeftY, -0.5)));
assert_eq!(map.axis2("move"), (0.0, -0.5));
}
#[test]
fn the_dpad_resolves_to_an_axis2_like_wasd() {
use PadButton::{DpadDown, DpadLeft, DpadRight, DpadUp};
let mut map = ActionMap::new();
map.declare(action("move", ActionKind::Axis2, vec![Binding::PadDpad]));
map.declare(action(
"walk",
ActionKind::Axis2,
vec![Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
}],
));
let key = |button| match button {
DpadUp => KeyCode::KeyW,
DpadDown => KeyCode::KeyS,
DpadLeft => KeyCode::KeyA,
_ => KeyCode::KeyD,
};
let half = std::f32::consts::FRAC_1_SQRT_2;
let cases: [(&[PadButton], (f32, f32)); 9] = [
(&[DpadUp], (0.0, 1.0)),
(&[DpadDown], (0.0, -1.0)),
(&[DpadLeft], (-1.0, 0.0)),
(&[DpadRight], (1.0, 0.0)),
(&[DpadUp, DpadRight], (half, half)),
(&[DpadUp, DpadLeft], (-half, half)),
(&[DpadDown, DpadRight], (half, -half)),
(&[DpadDown, DpadLeft], (-half, -half)),
(&[DpadUp, DpadDown], (0.0, 0.0)),
];
for (buttons, expected) in cases {
map.gamepad_event(&state(PAD, holding(buttons)));
for &button in buttons {
map.key_event(key(button), true);
}
assert!(
close(map.axis2("move"), expected),
"{buttons:?}: {:?}",
map.axis2("move"),
);
assert_eq!(map.axis2("move"), map.axis2("walk"), "{buttons:?}");
for &button in buttons {
map.key_event(key(button), false);
}
}
map.gamepad_event(&state(PAD, holding(&[])));
assert_eq!(map.axis2("move"), (0.0, 0.0), "let go");
}
#[test]
fn the_dpad_on_a_button_is_down_while_any_direction_is() {
let mut map = ActionMap::new();
map.declare(action("any", ActionKind::Button, vec![Binding::PadDpad]));
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
assert!(!map.button_held("any"));
for button in PadButton::DPAD {
map.gamepad_event(&state(PAD, holding(&[button])));
assert!(map.button_held("any"), "{button:?}");
map.gamepad_event(&state(PAD, holding(&[])));
assert!(!map.button_held("any"));
}
}
#[test]
fn keys_the_dpad_and_the_stick_sum_into_one_unit_disc() {
let mut map = ActionMap::new();
map.declare(action(
"move",
ActionKind::Axis2,
vec![
Binding::Wasd {
up: KeyCode::KeyW,
down: KeyCode::KeyS,
left: KeyCode::KeyA,
right: KeyCode::KeyD,
},
Binding::PadDpad,
Binding::PadStick {
stick: Stick::Left,
deadzone: 0.0,
},
],
));
let dpad = |button| holding(&[button]);
let half = std::f32::consts::FRAC_1_SQRT_2;
map.gamepad_event(&state(
PAD,
with_axis(dpad(PadButton::DpadRight), PadAxis::LeftX, 0.5),
));
assert!(
close(map.axis2("move"), (1.0, 0.0)),
"{:?}",
map.axis2("move")
);
map.gamepad_event(&state(
PAD,
with_axis(dpad(PadButton::DpadRight), PadAxis::LeftY, 1.0),
));
assert!(
close(map.axis2("move"), (half, half)),
"{:?}",
map.axis2("move")
);
map.gamepad_event(&state(
PAD,
with_axis(dpad(PadButton::DpadLeft), PadAxis::LeftX, 0.5),
));
assert!(
close(map.axis2("move"), (-0.5, 0.0)),
"{:?}",
map.axis2("move")
);
map.gamepad_event(&state(PAD, dpad(PadButton::DpadRight)));
map.key_event(KeyCode::KeyA, true);
assert_eq!(map.axis2("move"), (0.0, 0.0));
map.key_event(KeyCode::KeyA, false);
map.key_event(KeyCode::KeyW, true);
map.gamepad_event(&state(
PAD,
with_axis(dpad(PadButton::DpadUp), PadAxis::LeftY, 1.0),
));
assert!(
close(map.axis2("move"), (0.0, 1.0)),
"{:?}",
map.axis2("move")
);
}
#[test]
fn a_dpad_press_makes_the_pad_the_last_device() {
let mut map = ActionMap::new();
map.declare(action("move", ActionKind::Axis2, vec![Binding::PadDpad]));
map.key_event(KeyCode::KeyQ, true);
assert_eq!(map.last_device(), Some(Device::Keyboard));
map.gamepad_event(&state(PAD, holding(&[PadButton::DpadLeft])));
assert_eq!(map.last_device(), Some(Device::Gamepad));
map.key_event(KeyCode::KeyQ, false);
map.key_event(KeyCode::KeyQ, true);
map.gamepad_event(&state(PAD, holding(&[])));
assert_eq!(
map.last_device(),
Some(Device::Keyboard),
"letting go of the d-pad is not the pad speaking",
);
}
#[test]
fn a_pushed_dpad_takes_its_four_buttons_from_the_context_beneath() {
let mut map = jump_map();
map.declare(action(
"up",
ActionKind::Button,
vec![Binding::PadButton(PadButton::DpadUp)],
));
map.declare_in(
"menu",
action("move", ActionKind::Axis2, vec![Binding::PadDpad]),
);
map.push_context("menu").expect("declared");
map.gamepad_event(&state(PAD, holding(&[PadButton::DpadUp, PadButton::South])));
assert_eq!(map.axis2("move"), (0.0, 1.0));
assert!(!map.button_held("up"), "the menu owns DpadUp");
assert!(map.button_held("jump"), "and not South");
}
#[test]
fn the_last_device_becomes_the_pad_on_activity_and_not_on_release() {
let mut map = ActionMap::new();
map.gamepad_event(&GamepadEvent::Connected {
id: PAD,
kind: PadKind::Xbox,
});
assert_eq!(map.last_device(), None, "plugging in is not speaking");
map.gamepad_event(&state(PAD, holding(&[PadButton::North])));
assert_eq!(
map.last_device(),
Some(Device::Gamepad),
"nothing binds North"
);
map.key_event(KeyCode::KeyQ, true);
map.gamepad_event(&state(PAD, holding(&[])));
assert_eq!(
map.last_device(),
Some(Device::Keyboard),
"letting go of a pad button is not the pad speaking",
);
let rest = GamepadSnapshot::neutral(PadKind::Xbox);
let drift = with_axis(rest, PadAxis::RightX, 0.3);
map.gamepad_event(&state(PAD, drift));
assert_eq!(
map.last_device(),
Some(Device::Keyboard),
"drift is not a push"
);
let over = with_axis(rest, PadAxis::RightX, 0.9);
map.gamepad_event(&state(PAD, over));
assert_eq!(map.last_device(), Some(Device::Gamepad), "a push is");
map.key_event(KeyCode::KeyQ, true);
map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::RightX, 0.95)));
assert_eq!(
map.last_device(),
Some(Device::Keyboard),
"a stick held over already spoke",
);
map.gamepad_event(&state(PAD, rest));
assert_eq!(
map.last_device(),
Some(Device::Keyboard),
"nor does centring"
);
map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::LeftTrigger, 0.8)));
assert_eq!(
map.last_device(),
Some(Device::Gamepad),
"a trigger pull is"
);
}
#[test]
fn focus_loss_releases_a_held_pad_button_until_it_is_let_go() {
let mut map = jump_map();
map.begin_tick(TICK);
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
assert!(map.button_held("jump"));
map.begin_tick(TICK);
map.release_gamepads();
assert!(map.just_released("jump"));
assert!(!map.button_held("jump"));
assert_eq!(
map.gamepads().collect::<Vec<_>>(),
[(PAD, &GamepadSnapshot::neutral(PadKind::Xbox))],
"still connected, at rest",
);
map.begin_tick(TICK);
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
assert!(!map.button_held("jump"), "withheld until released");
map.gamepad_event(&state(PAD, holding(&[])));
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
assert!(map.just_pressed("jump"), "a fresh press after the lift");
}
#[test]
fn focus_loss_centres_a_stick_until_the_next_snapshot() {
let mut map = ActionMap::new();
map.declare(action(
"move",
ActionKind::Axis2,
vec![Binding::PadStick {
stick: Stick::Left,
deadzone: 0.0,
}],
));
let up = with_axis(GamepadSnapshot::neutral(PadKind::Xbox), PadAxis::LeftY, 1.0);
map.gamepad_event(&state(PAD, up));
map.release_gamepads();
assert_eq!(map.axis2("move"), (0.0, 0.0));
map.gamepad_event(&state(PAD, up));
assert_eq!(map.axis2("move"), (0.0, 1.0));
}
#[test]
fn a_disconnected_pad_releases_what_it_held() {
let mut map = jump_map();
map.declare(action(
"move",
ActionKind::Axis2,
vec![Binding::PadStick {
stick: Stick::Left,
deadzone: 0.1,
}],
));
map.begin_tick(TICK);
let held = with_axis(holding(&[PadButton::South]), PadAxis::LeftX, 1.0);
map.gamepad_event(&state(PAD, held));
assert!(map.button_held("jump"));
assert_eq!(map.axis2("move"), (1.0, 0.0));
map.begin_tick(TICK);
map.gamepad_event(&GamepadEvent::Disconnected { id: PAD });
assert!(map.just_released("jump"));
assert_eq!(map.axis2("move"), (0.0, 0.0));
assert_eq!(map.gamepads().count(), 0);
map.begin_tick(TICK);
assert!(
!map.button_held("jump"),
"and not re-pressed by the next tick"
);
}
#[test]
fn a_non_finite_snapshot_is_dropped() {
let mut map = jump_map();
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
let poisoned = with_axis(holding(&[]), PadAxis::LeftX, f32::NAN);
map.gamepad_event(&state(PAD, poisoned));
assert!(map.button_held("jump"));
}
#[test]
fn a_pad_button_is_routed_and_withheld_like_a_key() {
let mut map = jump_map();
map.declare_in(
"menu",
action(
"accept",
ActionKind::Button,
vec![Binding::PadButton(PadButton::South)],
),
);
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
map.push_context("menu").expect("declared");
assert!(map.just_released("jump"), "the menu took South");
assert!(!map.button_held("accept"), "withheld until released");
map.gamepad_event(&state(PAD, holding(&[])));
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
assert!(map.just_pressed("accept"));
assert!(!map.button_held("jump"));
}
#[test]
fn an_unusable_deadzone_is_refused() {
let mut map = ActionMap::new();
for deadzone in [1.0, -0.1, f32::NAN] {
assert_eq!(
map.try_declare(action(
"move",
ActionKind::Axis2,
vec![Binding::PadStick {
stick: Stick::Left,
deadzone,
}],
)),
Err(ActionMapError::InvalidDeadzone("move".to_owned())),
);
}
map.declare(action("fire", ActionKind::Button, Vec::new()));
assert_eq!(
map.rebind(
"fire",
vec![Binding::PadTrigger {
trigger: Trigger::Left,
threshold: f32::INFINITY,
}],
),
Err(ActionMapError::InvalidDeadzone("fire".to_owned())),
);
}
#[test]
fn allocated_ids_are_distinct() {
let a = GamepadId::allocate();
let b = GamepadId::allocate();
assert_ne!(a, b);
}
fn chord_map() -> ActionMap {
let mut map = jump_map();
map.declare(action(
"vault",
ActionKind::Button,
vec![Binding::PadChord {
modifier: PadButton::LeftShoulder,
button: PadButton::South,
}],
));
map.declare(action(
"lean",
ActionKind::Button,
vec![Binding::PadButton(PadButton::LeftShoulder)],
));
map
}
#[test]
fn a_pad_chord_takes_its_button_and_leaves_its_modifier_alone() {
let mut map = chord_map();
map.begin_tick(TICK);
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
assert!(map.button_held("jump"));
assert!(!map.button_held("vault"));
map.begin_tick(TICK);
map.gamepad_event(&state(PAD, holding(&[])));
map.gamepad_event(&state(
PAD,
holding(&[PadButton::LeftShoulder, PadButton::South]),
));
assert!(map.button_held("vault"));
assert!(!map.button_held("jump"), "the plain binding fired too");
assert!(map.button_held("lean"), "the modifier was consumed");
}
#[test]
fn a_modifier_pressed_over_a_held_button_hands_the_button_to_the_chord() {
let mut map = chord_map();
map.begin_tick(TICK);
map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
assert!(map.button_held("jump"));
map.begin_tick(TICK);
map.gamepad_event(&state(
PAD,
holding(&[PadButton::South, PadButton::LeftShoulder]),
));
assert!(map.just_released("jump"));
assert!(map.just_pressed("vault"));
}
#[test]
fn a_pad_chord_drives_an_axis1() {
let mut map = jump_map();
map.declare(action(
"throttle",
ActionKind::Axis1,
vec![Binding::PadChord {
modifier: PadButton::RightShoulder,
button: PadButton::North,
}],
));
map.gamepad_event(&state(PAD, holding(&[PadButton::North])));
assert!(map.axis1("throttle").abs() < 1e-6, "no modifier, no chord");
map.gamepad_event(&state(
PAD,
holding(&[PadButton::RightShoulder, PadButton::North]),
));
assert!((map.axis1("throttle") - 1.0).abs() < 1e-6);
}
#[test]
fn a_pad_chord_shadows_only_in_the_context_that_owns_its_button() {
let mut map = jump_map();
map.declare_in(
"vehicle",
action(
"boost",
ActionKind::Button,
vec![Binding::PadChord {
modifier: PadButton::LeftShoulder,
button: PadButton::South,
}],
),
);
let both = holding(&[PadButton::LeftShoulder, PadButton::South]);
map.gamepad_event(&state(PAD, both));
assert!(
map.button_held("jump"),
"a chord off the stack shadowed South"
);
map.gamepad_event(&state(PAD, holding(&[])));
map.push_context("vehicle").unwrap();
map.gamepad_event(&state(PAD, both));
assert!(map.button_held("boost"));
assert!(!map.button_held("jump"));
}
}