pub use crate::float_axis::{stick_axis, trigger_axis};
use crate::{GamepadSnapshot, PadAxis, PadButton, PadKind};
use crate::{product_name, usb};
pub const STANDARD_BUTTONS: usize = 17;
pub const STANDARD_AXES: usize = 4;
pub const VALUES: usize = STANDARD_BUTTONS + STANDARD_AXES;
const LEFT_TRIGGER: usize = 6;
const RIGHT_TRIGGER: usize = 7;
const LEFT_X: usize = STANDARD_BUTTONS;
const LEFT_Y: usize = STANDARD_BUTTONS + 1;
const RIGHT_X: usize = STANDARD_BUTTONS + 2;
const RIGHT_Y: usize = STANDARD_BUTTONS + 3;
const BUTTONS: [(usize, PadButton); 15] = [
(0, PadButton::South),
(1, PadButton::East),
(2, PadButton::West),
(3, PadButton::North),
(4, PadButton::LeftShoulder),
(5, PadButton::RightShoulder),
(8, PadButton::Select),
(9, PadButton::Start),
(10, PadButton::LeftStick),
(11, PadButton::RightStick),
(12, PadButton::DpadUp),
(13, PadButton::DpadDown),
(14, PadButton::DpadLeft),
(15, PadButton::DpadRight),
(16, PadButton::Guide),
];
pub(super) fn snapshot_of(pressed: u32, values: &[f32; VALUES], kind: PadKind) -> GamepadSnapshot {
let mut snapshot = GamepadSnapshot::neutral(kind);
snapshot.buttons = BUTTONS
.iter()
.filter(|&&(index, _)| pressed & (1 << index) != 0)
.map(|&(_, button)| button)
.collect();
snapshot.axes[PadAxis::LeftX as usize] = stick_axis(values[LEFT_X]);
snapshot.axes[PadAxis::LeftY as usize] = -stick_axis(values[LEFT_Y]);
snapshot.axes[PadAxis::RightX as usize] = stick_axis(values[RIGHT_X]);
snapshot.axes[PadAxis::RightY as usize] = -stick_axis(values[RIGHT_Y]);
snapshot.axes[PadAxis::LeftTrigger as usize] = trigger_axis(values[LEFT_TRIGGER]);
snapshot.axes[PadAxis::RightTrigger as usize] = trigger_axis(values[RIGHT_TRIGGER]);
snapshot
}
pub(super) fn kind_of_id(id: &str) -> PadKind {
if let Some((vendor, product)) = usb_ids(id) {
return usb::kind_of(vendor, product);
}
product_name::kind_of_name(id)
}
fn usb_ids(id: &str) -> Option<(u16, u16)> {
if let Some(at) = id.find("Vendor: ") {
let rest = &id[at + "Vendor: ".len()..];
let vendor = hex_prefix(rest)?;
let product_at = rest.find("Product: ")?;
let product = hex_prefix(&rest[product_at + "Product: ".len()..])?;
return Some((vendor, product));
}
let mut fields = id.splitn(3, '-');
let vendor = fields.next().and_then(hex_field)?;
let product = fields.next().and_then(hex_field)?;
fields.next()?;
Some((vendor, product))
}
fn hex_prefix(text: &str) -> Option<u16> {
let digits = text
.find(|c: char| !c.is_ascii_hexdigit())
.unwrap_or(text.len());
hex_field(&text[..digits])
}
fn hex_field(field: &str) -> Option<u16> {
let valid = (1..=4).contains(&field.len()) && field.bytes().all(|b| b.is_ascii_hexdigit());
valid.then(|| u16::from_str_radix(field, 16).ok()).flatten()
}
#[cfg(test)]
pub(super) mod tests {
use super::*;
use crate::PadButtons;
pub(in crate::web_gamepad) const AT_REST: [f32; VALUES] = [0.0; VALUES];
#[test]
fn every_standard_button_maps_to_its_position() {
for &(index, button) in &BUTTONS {
let snapshot = snapshot_of(1 << index, &AT_REST, PadKind::Generic);
assert_eq!(
snapshot.buttons,
[button].into_iter().collect::<PadButtons>(),
"buttons[{index}]"
);
}
assert_eq!(BUTTONS[14], (16, PadButton::Guide), "16 is the home button");
for trigger in [LEFT_TRIGGER, RIGHT_TRIGGER] {
let snapshot = snapshot_of(1 << trigger, &AT_REST, PadKind::Generic);
assert!(snapshot.buttons.is_empty(), "buttons[{trigger}] is an axis");
}
let past_the_mapping = snapshot_of(1 << STANDARD_BUTTONS, &AT_REST, PadKind::Generic);
assert!(past_the_mapping.buttons.is_empty(), "bit 17 names nothing");
}
#[test]
fn the_triggers_are_buttons_six_and_seven_by_value() {
let mut values = AT_REST;
values[6] = 0.25;
values[7] = 0.75;
let snapshot = snapshot_of(0, &values, PadKind::Generic);
assert_eq!(snapshot.axis(PadAxis::LeftTrigger), 0.25);
assert_eq!(snapshot.axis(PadAxis::RightTrigger), 0.75);
assert!(snapshot.buttons.is_empty());
}
#[test]
fn stick_y_is_flipped_to_up() {
let mut values = AT_REST;
values[STANDARD_BUTTONS] = 0.5;
values[STANDARD_BUTTONS + 1] = -1.0; values[STANDARD_BUTTONS + 2] = -0.25;
values[STANDARD_BUTTONS + 3] = 0.75; let snapshot = snapshot_of(0, &values, PadKind::Generic);
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_eq!(snapshot_of(0, &AT_REST, PadKind::Xbox), {
GamepadSnapshot::neutral(PadKind::Xbox)
});
}
#[test]
fn values_are_clamped_and_finite() {
let mut values = [f32::NAN; VALUES];
values[LEFT_TRIGGER] = 1.5;
values[LEFT_X] = -7.0;
let clamped = snapshot_of(0, &values, PadKind::Generic);
assert_eq!(clamped.axis(PadAxis::LeftTrigger), 1.0);
assert_eq!(clamped.axis(PadAxis::LeftX), -1.0);
let snapshot = snapshot_of(0, &[f32::NAN; VALUES], PadKind::Generic);
assert!(snapshot.is_finite());
assert_eq!(snapshot, GamepadSnapshot::neutral(PadKind::Generic));
}
#[test]
fn the_id_names_the_family() {
let cases = [
(
"DualSense Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 0ce6)",
PadKind::PlayStation,
),
(
"Xbox Wireless Controller (STANDARD GAMEPAD Vendor: 045e Product: 0b13)",
PadKind::Xbox,
),
(
"Steam Deck (Vendor: 28de Product: 1205)",
PadKind::SteamDeck,
),
("057e-2009-Pro Controller", PadKind::Switch),
("45e-28e-Xbox 360 Wired Controller", PadKind::Xbox),
("28de-11ff-Steam Virtual Gamepad", PadKind::Xbox),
(
"Xbox 360 Controller (XInput STANDARD GAMEPAD)",
PadKind::Xbox,
),
("DualSense Wireless Controller", PadKind::PlayStation),
("Nintendo Switch Pro Controller", PadKind::Switch),
(
"Unknown Gamepad (Vendor: 1234 Product: 5678)",
PadKind::Generic,
),
("Some Arcade Stick", PadKind::Generic),
("", PadKind::Generic),
];
for (id, kind) in cases {
assert_eq!(kind_of_id(id), kind, "{id:?}");
}
}
#[test]
fn only_hex_fields_are_ids() {
assert_eq!(usb_ids("Joy-Con (L)"), None);
assert_eq!(usb_ids("054c-0ce6-DualSense"), Some((0x054c, 0x0ce6)));
assert_eq!(usb_ids("12345-0ce6-Too long"), None);
assert_eq!(usb_ids("054c-0ce6"), None, "no name after the ids");
assert_eq!(
usb_ids("Pad (Vendor: 054c Product: zz)"),
None,
"a vendor with no product"
);
}
}