#[cfg(target_os = "macos")]
mod ffi;
#[cfg(target_os = "macos")]
mod macos;
pub use crate::float_axis::{stick_axis, trigger_axis};
use crate::product_name::kind_of_name;
use crate::{GamepadEvent, GamepadId, GamepadSnapshot, PadAxis, PadButton, PadKind};
use core::ffi::CStr;
#[cfg(target_os = "macos")]
pub use macos::GameController;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GameControllerError {
Unavailable,
}
impl std::fmt::Display for GameControllerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unavailable => write!(f, "GameController.framework has no GCController class"),
}
}
}
impl std::error::Error for GameControllerError {}
type Path = &'static [&'static CStr];
const BUTTONS: [(Path, PadButton); 15] = [
(&[c"buttonA"], PadButton::South),
(&[c"buttonB"], PadButton::East),
(&[c"buttonX"], PadButton::West),
(&[c"buttonY"], PadButton::North),
(&[c"leftShoulder"], PadButton::LeftShoulder),
(&[c"rightShoulder"], PadButton::RightShoulder),
(&[c"leftThumbstickButton"], PadButton::LeftStick),
(&[c"rightThumbstickButton"], PadButton::RightStick),
(&[c"buttonMenu"], PadButton::Start),
(&[c"buttonOptions"], PadButton::Select),
(&[c"buttonHome"], PadButton::Guide),
(&[c"dpad", c"up"], PadButton::DpadUp),
(&[c"dpad", c"down"], PadButton::DpadDown),
(&[c"dpad", c"left"], PadButton::DpadLeft),
(&[c"dpad", c"right"], PadButton::DpadRight),
];
const AXES: [(Path, PadAxis); 6] = [
(&[c"leftThumbstick", c"xAxis"], PadAxis::LeftX),
(&[c"leftThumbstick", c"yAxis"], PadAxis::LeftY),
(&[c"rightThumbstick", c"xAxis"], PadAxis::RightX),
(&[c"rightThumbstick", c"yAxis"], PadAxis::RightY),
(&[c"leftTrigger"], PadAxis::LeftTrigger),
(&[c"rightTrigger"], PadAxis::RightTrigger),
];
#[derive(Clone, Copy, Debug, Default, PartialEq)]
struct Reading {
pressed: [bool; BUTTONS.len()],
values: [f32; AXES.len()],
}
fn snapshot_of(reading: &Reading, kind: PadKind) -> GamepadSnapshot {
let mut snapshot = GamepadSnapshot::neutral(kind);
snapshot.buttons = BUTTONS
.iter()
.zip(reading.pressed)
.filter(|&(_, pressed)| pressed)
.map(|(&(_, button), _)| button)
.collect();
for (&(_, axis), value) in AXES.iter().zip(reading.values) {
snapshot.axes[axis as usize] = match axis {
PadAxis::LeftTrigger | PadAxis::RightTrigger => trigger_axis(value),
PadAxis::LeftX | PadAxis::LeftY | PadAxis::RightX | PadAxis::RightY => {
stick_axis(value)
}
};
}
snapshot
}
#[must_use]
pub fn kind_of(product_category: Option<&str>, vendor_name: Option<&str>) -> PadKind {
[product_category, vendor_name]
.into_iter()
.flatten()
.map(kind_of_name)
.find(|&kind| kind != PadKind::Generic)
.unwrap_or(PadKind::Generic)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct Key(usize);
#[derive(Clone, Copy, Debug)]
struct Seen {
key: Key,
kind: PadKind,
reading: Reading,
}
trait Source {
fn read(&mut self, into: &mut Vec<Seen>);
}
#[derive(Clone, Copy, Debug)]
struct Slot {
key: Key,
id: GamepadId,
kind: PadKind,
last: GamepadSnapshot,
}
#[derive(Debug, Default)]
struct Poller {
slots: Vec<Slot>,
seen: Vec<Seen>,
}
impl Poller {
fn poll(&mut self, source: &mut impl Source, emit: &mut impl FnMut(GamepadEvent)) {
source.read(&mut self.seen);
let seen = &self.seen;
self.slots.retain(|slot| {
let stays = seen.iter().any(|controller| controller.key == slot.key);
if !stays {
emit(GamepadEvent::Disconnected { id: slot.id });
}
stays
});
for controller in seen {
let at = match self
.slots
.iter()
.position(|slot| slot.key == controller.key)
{
Some(at) => at,
None => {
let id = GamepadId::allocate();
let kind = controller.kind;
emit(GamepadEvent::Connected { id, kind });
self.slots.push(Slot {
key: controller.key,
id,
kind,
last: GamepadSnapshot::neutral(kind),
});
self.slots.len() - 1
}
};
let slot = &mut self.slots[at];
let snapshot = snapshot_of(&controller.reading, slot.kind);
if snapshot != slot.last {
slot.last = snapshot;
emit(GamepadEvent::State {
id: slot.id,
snapshot,
});
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ActionDecl, ActionKind, ActionMap, Binding, PadButtons};
fn button_row(path: &[&CStr]) -> usize {
BUTTONS
.iter()
.position(|&(row, _)| row == path)
.unwrap_or_else(|| panic!("no button row for {path:?}"))
}
fn axis_row(path: &[&CStr]) -> usize {
AXES.iter()
.position(|&(row, _)| row == path)
.unwrap_or_else(|| panic!("no axis row for {path:?}"))
}
fn pressing(path: &[&CStr]) -> Reading {
let mut reading = Reading::default();
reading.pressed[button_row(path)] = true;
reading
}
#[test]
fn every_button_element_maps_to_its_position() {
let expected: [(&[&CStr], PadButton); 15] = [
(&[c"buttonA"], PadButton::South),
(&[c"buttonB"], PadButton::East),
(&[c"buttonX"], PadButton::West),
(&[c"buttonY"], PadButton::North),
(&[c"leftShoulder"], PadButton::LeftShoulder),
(&[c"rightShoulder"], PadButton::RightShoulder),
(&[c"leftThumbstickButton"], PadButton::LeftStick),
(&[c"rightThumbstickButton"], PadButton::RightStick),
(&[c"buttonMenu"], PadButton::Start),
(&[c"buttonOptions"], PadButton::Select),
(&[c"buttonHome"], PadButton::Guide),
(&[c"dpad", c"up"], PadButton::DpadUp),
(&[c"dpad", c"down"], PadButton::DpadDown),
(&[c"dpad", c"left"], PadButton::DpadLeft),
(&[c"dpad", c"right"], PadButton::DpadRight),
];
for (path, button) in expected {
assert_eq!(
snapshot_of(&pressing(path), PadKind::Generic).buttons,
[button].into_iter().collect::<PadButtons>(),
"{path:?}"
);
}
let every: PadButtons = PadButton::ALL.into_iter().collect();
let all_pressed = Reading {
pressed: [true; BUTTONS.len()],
..Reading::default()
};
assert_eq!(
snapshot_of(&all_pressed, PadKind::Generic).buttons,
every,
"every seam button has an element"
);
}
#[test]
fn analog_elements_map_with_y_up_and_triggers_by_value() {
let mut reading = Reading::default();
reading.values[axis_row(&[c"leftTrigger"])] = 0.25;
reading.values[axis_row(&[c"rightTrigger"])] = 0.75;
reading.values[axis_row(&[c"leftThumbstick", c"xAxis"])] = 0.5;
reading.values[axis_row(&[c"leftThumbstick", c"yAxis"])] = 1.0; reading.values[axis_row(&[c"rightThumbstick", c"xAxis"])] = -0.25;
reading.values[axis_row(&[c"rightThumbstick", c"yAxis"])] = -0.75; let snapshot = snapshot_of(&reading, PadKind::Xbox);
assert_eq!(snapshot.axis(PadAxis::LeftTrigger), 0.25);
assert_eq!(snapshot.axis(PadAxis::RightTrigger), 0.75);
assert_eq!(snapshot.axis(PadAxis::LeftX), 0.5);
assert_eq!(snapshot.axis(PadAxis::LeftY), 1.0, "up is +Y");
assert_eq!(snapshot.axis(PadAxis::RightX), -0.25);
assert_eq!(snapshot.axis(PadAxis::RightY), -0.75, "down is -Y");
assert!(snapshot.buttons.is_empty(), "a pulled trigger is an axis");
assert_eq!(snapshot.kind, PadKind::Xbox);
assert_eq!(
snapshot_of(&Reading::default(), PadKind::Switch),
GamepadSnapshot::neutral(PadKind::Switch)
);
}
#[test]
fn analog_values_are_clamped_and_finite() {
let mut reading = Reading {
values: [f32::NAN; AXES.len()],
..Reading::default()
};
reading.values[axis_row(&[c"leftTrigger"])] = 1.5;
reading.values[axis_row(&[c"rightThumbstick", c"xAxis"])] = -3.0;
let snapshot = snapshot_of(&reading, PadKind::Generic);
assert!(snapshot.is_finite());
assert_eq!(snapshot.axis(PadAxis::LeftTrigger), 1.0);
assert_eq!(snapshot.axis(PadAxis::RightX), -1.0);
assert_eq!(snapshot.axis(PadAxis::LeftY), 0.0, "NaN reads as rest");
}
#[test]
fn the_category_or_vendor_names_the_family() {
let cases = [
(Some("DualSense"), None, PadKind::PlayStation),
(Some("DualShock 4"), None, PadKind::PlayStation),
(Some("Xbox One"), None, PadKind::Xbox),
(Some("Switch Pro Controller"), None, PadKind::Switch),
(
Some("Nintendo Switch Joy-Con (L/R)"),
Some("Joy-Con (L/R)"),
PadKind::Switch,
),
(Some("MFi"), Some("Xbox Wireless Controller"), PadKind::Xbox),
(
Some("HID"),
Some("DUALSHOCK 4 Wireless"),
PadKind::PlayStation,
),
(None, Some("Pro Controller"), PadKind::Switch),
(Some("Xbox One"), Some("DualSense"), PadKind::Xbox),
(Some("MFi"), Some("SteelSeries Nimbus+"), PadKind::Generic),
(Some("HID"), None, PadKind::Generic),
(None, None, PadKind::Generic),
];
for (category, vendor, kind) in cases {
assert_eq!(kind_of(category, vendor), kind, "{category:?} / {vendor:?}");
}
}
#[derive(Debug, Default)]
struct Fake {
listed: Vec<Seen>,
reads: u32,
}
impl Fake {
fn list(&mut self, key: usize, kind: PadKind, reading: Reading) {
self.listed.push(Seen {
key: Key(key),
kind,
reading,
});
}
fn set(&mut self, key: usize, reading: Reading) {
let controller = self
.listed
.iter_mut()
.find(|controller| controller.key == Key(key))
.expect("listed");
controller.reading = reading;
}
fn unlist(&mut self, key: usize) {
self.listed.retain(|controller| controller.key != Key(key));
}
}
impl Source for Fake {
fn read(&mut self, into: &mut Vec<Seen>) {
self.reads += 1;
into.clone_from(&self.listed);
}
}
fn poll(poller: &mut Poller, fake: &mut Fake) -> Vec<GamepadEvent> {
let mut events = Vec::new();
poller.poll(fake, &mut |event| events.push(event));
events
}
#[test]
fn controller_transitions_are_reported_once_each() {
let mut poller = Poller::default();
let mut fake = Fake::default();
assert_eq!(poll(&mut poller, &mut fake), [], "nothing listed");
fake.list(0x1000, PadKind::PlayStation, pressing(&[c"buttonA"]));
let events = poll(&mut poller, &mut fake);
let [
GamepadEvent::Connected { id, kind },
GamepadEvent::State { id: same, snapshot },
] = events[..]
else {
panic!("a connection and its first state: {events:?}");
};
assert_eq!((kind, same), (PadKind::PlayStation, id));
assert!(snapshot.buttons.contains(PadButton::South));
assert_eq!(snapshot.kind, PadKind::PlayStation);
assert_eq!(poll(&mut poller, &mut fake), [], "unchanged");
assert_eq!(poll(&mut poller, &mut fake), [], "still unchanged");
fake.set(0x1000, Reading::default());
assert_eq!(
poll(&mut poller, &mut fake),
[GamepadEvent::State {
id,
snapshot: GamepadSnapshot::neutral(PadKind::PlayStation),
}],
);
fake.unlist(0x1000);
assert_eq!(
poll(&mut poller, &mut fake),
[GamepadEvent::Disconnected { id }]
);
assert_eq!(poll(&mut poller, &mut fake), [], "gone stays gone");
fake.list(0x2000, PadKind::Xbox, Reading::default());
let events = poll(&mut poller, &mut fake);
let [GamepadEvent::Connected { id: again, kind }] = events[..] else {
panic!("a reconnection at rest is one event: {events:?}");
};
assert_ne!(again, id, "a new controller object is a new pad");
assert_eq!(kind, PadKind::Xbox);
assert_eq!(fake.reads, 8, "the list is read on every poll");
}
#[test]
fn identity_not_list_position_names_a_pad() {
let mut poller = Poller::default();
let mut fake = Fake::default();
fake.list(1, PadKind::Xbox, Reading::default());
fake.list(2, PadKind::Switch, Reading::default());
let events = poll(&mut poller, &mut fake);
let [
GamepadEvent::Connected { id: first, .. },
GamepadEvent::Connected { id: second, .. },
] = events[..]
else {
panic!("{events:?}");
};
fake.unlist(1);
fake.list(3, PadKind::PlayStation, Reading::default());
fake.set(2, pressing(&[c"buttonB"]));
let events = poll(&mut poller, &mut fake);
let [
GamepadEvent::Disconnected { id: gone },
GamepadEvent::State {
id: pressed,
snapshot,
},
GamepadEvent::Connected { id: third, kind },
] = events[..]
else {
panic!("{events:?}");
};
assert_eq!(gone, first);
assert_eq!(pressed, second, "still the same pad at a new position");
assert!(snapshot.buttons.contains(PadButton::East));
assert_eq!(snapshot.kind, PadKind::Switch);
assert_ne!(third, second);
assert_eq!(kind, PadKind::PlayStation);
}
#[test]
fn polled_events_drive_an_action_map() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "jump".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::PadButton(PadButton::South)],
});
let mut poller = Poller::default();
let mut fake = Fake::default();
fake.list(7, PadKind::Xbox, pressing(&[c"buttonA"]));
poller.poll(&mut fake, &mut |event| map.gamepad_event(&event));
assert!(map.just_pressed("jump"));
fake.unlist(7);
poller.poll(&mut fake, &mut |event| map.gamepad_event(&event));
assert!(map.just_released("jump"), "a controller leaving releases");
}
}