mod map;
pub mod shim;
use crate::{GamepadEvent, GamepadId, GamepadSnapshot, PadKind};
pub use map::{STANDARD_AXES, STANDARD_BUTTONS, VALUES, stick_axis, trigger_axis};
pub const ID_CAPACITY: usize = 256;
#[derive(Clone, Copy, Debug, PartialEq)]
struct Report {
index: u32,
connected: bool,
standard: bool,
pressed: u32,
values: [f32; VALUES],
id: [u8; ID_CAPACITY],
id_len: usize,
}
impl Report {
fn id(&self) -> &[u8] {
&self.id[..self.id_len]
}
fn name(&self) -> String {
String::from_utf8_lossy(self.id()).into_owned()
}
}
trait Source {
fn take(&mut self, into: &mut Vec<Report>) -> bool;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Unmapped {
pub index: u32,
pub id: String,
}
impl std::fmt::Display for Unmapped {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"pad {} ({:?}) has no standard mapping in this browser; ignoring it",
self.index, self.id
)
}
}
#[derive(Clone, Copy, Debug)]
enum Held {
Mapped {
pad: GamepadId,
kind: PadKind,
last: GamepadSnapshot,
},
Unmapped,
}
#[derive(Debug)]
struct Slot {
index: u32,
id: Vec<u8>,
standard: bool,
held: Held,
}
impl Slot {
fn is(&self, report: &Report) -> bool {
report.index == self.index && report.standard == self.standard && report.id() == self.id
}
}
#[derive(Debug, Default)]
struct Poller {
slots: Vec<Slot>,
reports: Vec<Report>,
}
impl Poller {
fn poll(
&mut self,
source: &mut impl Source,
emit: &mut impl FnMut(GamepadEvent),
) -> Vec<Unmapped> {
let mut unmapped = Vec::new();
if !source.take(&mut self.reports) {
return unmapped;
}
self.reports.retain(|report| report.connected);
self.reports.sort_by_key(|report| report.index);
self.reports.dedup_by_key(|report| report.index);
let reports = &self.reports;
self.slots.retain(|slot| {
let stays = reports.iter().any(|report| slot.is(report));
if let (false, Held::Mapped { pad, .. }) = (stays, slot.held) {
emit(GamepadEvent::Disconnected { id: pad });
}
stays
});
for report in reports {
let at = self.slots.partition_point(|slot| slot.index < report.index);
if self
.slots
.get(at)
.is_none_or(|slot| slot.index != report.index)
{
let held = if report.standard {
let pad = GamepadId::allocate();
let kind = map::kind_of_id(&report.name());
emit(GamepadEvent::Connected { id: pad, kind });
Held::Mapped {
pad,
kind,
last: GamepadSnapshot::neutral(kind),
}
} else {
unmapped.push(Unmapped {
index: report.index,
id: report.name(),
});
Held::Unmapped
};
let slot = Slot {
index: report.index,
id: report.id().to_vec(),
standard: report.standard,
held,
};
self.slots.insert(at, slot);
}
if let Held::Mapped { pad, kind, last } = &mut self.slots[at].held {
let snapshot = map::snapshot_of(report.pressed, &report.values, *kind);
if snapshot != *last {
*last = snapshot;
emit(GamepadEvent::State { id: *pad, snapshot });
}
}
}
unmapped
}
}
#[derive(Debug, Default)]
pub struct WebGamepads {
poller: Poller,
}
impl WebGamepads {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use = "an unmapped pad is otherwise ignored without a word"]
pub fn poll(&mut self, mut emit: impl FnMut(GamepadEvent)) -> Vec<Unmapped> {
self.poller.poll(&mut shim::Bridge, &mut emit)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ActionDecl, ActionKind, ActionMap, Binding, PadAxis, PadButton};
use map::tests::AT_REST;
fn pad(index: u32, id: &str) -> Report {
let mut bytes = [0; ID_CAPACITY];
bytes[..id.len()].copy_from_slice(id.as_bytes());
Report {
index,
connected: true,
standard: true,
pressed: 0,
values: AT_REST,
id: bytes,
id_len: id.len(),
}
}
const XBOX: &str = "Xbox Wireless Controller (STANDARD GAMEPAD Vendor: 045e Product: 0b13)";
const SONY: &str = "054c-0ce6-DualSense Wireless Controller";
#[derive(Debug, Default)]
struct Fake {
frame: Option<Vec<Report>>,
}
impl Source for Fake {
fn take(&mut self, into: &mut Vec<Report>) -> bool {
match self.frame.take() {
Some(frame) => {
*into = frame;
true
}
None => false,
}
}
}
fn poll(
poller: &mut Poller,
reports: Option<Vec<Report>>,
) -> (Vec<GamepadEvent>, Vec<Unmapped>) {
let mut fake = Fake { frame: reports };
let mut events = Vec::new();
let unmapped = poller.poll(&mut fake, &mut |event| events.push(event));
(events, unmapped)
}
#[test]
fn transitions_are_reported_once_each() {
let mut poller = Poller::default();
assert_eq!(poll(&mut poller, Some(vec![])), (vec![], vec![]));
let (events, unmapped) = poll(&mut poller, Some(vec![pad(0, XBOX)]));
assert!(unmapped.is_empty());
let [GamepadEvent::Connected { id, kind }] = events[..] else {
panic!("a pad at rest connects with no state: {events:?}");
};
assert_eq!(kind, PadKind::Xbox);
let mut pressed = pad(0, XBOX);
pressed.pressed = 1; pressed.values[STANDARD_BUTTONS + 1] = -1.0; let (events, _) = poll(&mut poller, Some(vec![pressed]));
let [GamepadEvent::State { id: same, snapshot }] = events[..] else {
panic!("one change, one state: {events:?}");
};
assert_eq!(same, id);
assert!(snapshot.buttons.contains(PadButton::South));
assert_eq!(snapshot.axis(PadAxis::LeftY), 1.0, "pushed up");
let (events, _) = poll(&mut poller, Some(vec![pressed]));
assert!(events.is_empty(), "the same snapshot again: {events:?}");
let (events, _) = poll(&mut poller, None);
assert!(events.is_empty(), "a frame with no report: {events:?}");
let (events, _) = poll(&mut poller, Some(vec![pad(0, XBOX)]));
assert_eq!(
events,
[GamepadEvent::State {
id,
snapshot: GamepadSnapshot::neutral(PadKind::Xbox),
}]
);
let (events, _) = poll(&mut poller, Some(vec![]));
assert_eq!(events, [GamepadEvent::Disconnected { id }]);
let (events, _) = poll(&mut poller, Some(vec![pad(0, XBOX)]));
let [GamepadEvent::Connected { id: again, .. }] = events[..] else {
panic!("{events:?}");
};
assert_ne!(again, id, "a pad that comes back is a new pad");
}
#[test]
fn a_pad_connected_mid_press_reports_its_state() {
let mut poller = Poller::default();
let mut held = pad(2, SONY);
held.pressed = 1 << 1;
held.values[7] = 1.0;
let (events, _) = poll(&mut poller, Some(vec![held]));
let [
GamepadEvent::Connected { id, kind },
GamepadEvent::State { id: same, snapshot },
] = events[..]
else {
panic!("{events:?}");
};
assert_eq!(kind, PadKind::PlayStation);
assert_eq!(same, id);
assert!(snapshot.buttons.contains(PadButton::East));
assert_eq!(snapshot.axis(PadAxis::RightTrigger), 1.0);
}
#[test]
fn a_pad_reporting_disconnected_is_gone() {
let mut poller = Poller::default();
let (events, _) = poll(&mut poller, Some(vec![pad(0, XBOX)]));
let [GamepadEvent::Connected { id, .. }] = events[..] else {
panic!("{events:?}");
};
let mut gone = pad(0, XBOX);
gone.connected = false;
assert_eq!(
poll(&mut poller, Some(vec![gone])).0,
[GamepadEvent::Disconnected { id }]
);
assert!(poll(&mut poller, Some(vec![gone])).0.is_empty());
}
#[test]
fn a_new_id_at_the_same_index_is_a_new_pad() {
let mut poller = Poller::default();
let (events, _) = poll(&mut poller, Some(vec![pad(0, XBOX), pad(1, XBOX)]));
let [
GamepadEvent::Connected { id: first, .. },
GamepadEvent::Connected { id: second, .. },
] = events[..]
else {
panic!("{events:?}");
};
let (events, _) = poll(&mut poller, Some(vec![pad(0, SONY), pad(1, XBOX)]));
let [
GamepadEvent::Disconnected { id: gone },
GamepadEvent::Connected { id: new, kind },
] = events[..]
else {
panic!("{events:?}");
};
assert_eq!(gone, first);
assert_ne!(new, first);
assert_ne!(new, second);
assert_eq!(kind, PadKind::PlayStation);
}
#[test]
fn an_unmapped_pad_is_skipped_and_returned_once() {
let mut poller = Poller::default();
let mut odd = pad(1, "Some Arcade Stick");
odd.standard = false;
odd.pressed = 1;
let (events, unmapped) = poll(&mut poller, Some(vec![odd]));
assert!(events.is_empty(), "{events:?}");
assert_eq!(
unmapped,
[Unmapped {
index: 1,
id: "Some Arcade Stick".to_owned(),
}]
);
assert_eq!(
unmapped[0].to_string(),
"pad 1 (\"Some Arcade Stick\") has no standard mapping in this browser; ignoring it"
);
assert_eq!(poll(&mut poller, Some(vec![odd])), (vec![], vec![]));
assert_eq!(poll(&mut poller, Some(vec![])), (vec![], vec![]));
assert_eq!(poll(&mut poller, Some(vec![odd])).1.len(), 1, "back again");
let mut remapped = odd;
remapped.standard = true;
let (events, unmapped) = poll(&mut poller, Some(vec![remapped]));
assert!(unmapped.is_empty());
assert!(
matches!(
events[..],
[GamepadEvent::Connected { .. }, GamepadEvent::State { .. }]
),
"the same pad, standard-mapped, is a pad: {events:?}"
);
}
#[test]
fn reports_are_taken_by_index() {
let mut poller = Poller::default();
let (events, _) = poll(
&mut poller,
Some(vec![pad(3, SONY), pad(1, XBOX), pad(3, SONY)]),
);
let [
GamepadEvent::Connected {
kind: PadKind::Xbox,
..
},
GamepadEvent::Connected {
kind: PadKind::PlayStation,
..
},
] = events[..]
else {
panic!("index 1 first, index 3 once: {events:?}");
};
}
#[test]
fn polled_events_drive_an_action_map() {
let mut map = ActionMap::new();
map.declare(ActionDecl {
name: "fire".to_owned(),
kind: ActionKind::Button,
bindings: vec![Binding::PadButton(PadButton::Guide)],
});
let mut poller = Poller::default();
let mut held = pad(0, XBOX);
held.pressed = 1 << 16;
let mut fake = Fake {
frame: Some(vec![held]),
};
let unmapped = poller.poll(&mut fake, &mut |event| map.gamepad_event(&event));
assert!(unmapped.is_empty());
assert!(map.just_pressed("fire"), "buttons[16] is Guide");
fake.frame = Some(vec![]);
let unmapped = poller.poll(&mut fake, &mut |event| map.gamepad_event(&event));
assert!(unmapped.is_empty());
assert!(map.just_released("fire"), "unplugging releases");
}
#[test]
fn the_entry_points_carry_a_pad_into_the_poll() {
use shim::{
__crcbl_web_pad, __crcbl_web_pad_begin, __crcbl_web_pad_id_capacity,
__crcbl_web_pad_id_ptr, __crcbl_web_pad_values_capacity, __crcbl_web_pad_values_ptr,
PAD_CONNECTED, PAD_STANDARD,
};
let mut pads = WebGamepads::new();
let mut events = Vec::new();
assert!(pads.poll(|event| events.push(event)).is_empty());
assert!(events.is_empty(), "nothing reported yet");
assert_eq!(__crcbl_web_pad_values_capacity() as usize, VALUES);
assert!(__crcbl_web_pad_id_capacity() as usize >= SONY.len());
let values_at = __crcbl_web_pad_values_ptr();
let id_at = __crcbl_web_pad_id_ptr();
assert_eq!(values_at, __crcbl_web_pad_values_ptr(), "the address stays");
let mut values = AT_REST;
values[6] = 0.5; values[STANDARD_BUTTONS + 3] = 1.0; unsafe {
core::ptr::copy_nonoverlapping(values.as_ptr(), values_at, VALUES);
core::ptr::copy_nonoverlapping(SONY.as_ptr(), id_at, SONY.len());
}
__crcbl_web_pad_begin();
__crcbl_web_pad(0, PAD_CONNECTED | PAD_STANDARD, 1 << 9, SONY.len() as u32);
assert!(pads.poll(|event| events.push(event)).is_empty());
let [
GamepadEvent::Connected { id, kind },
GamepadEvent::State { id: same, snapshot },
] = events[..]
else {
panic!("{events:?}");
};
assert_eq!(kind, PadKind::PlayStation, "the id crossed");
assert_eq!(same, id);
assert!(snapshot.buttons.contains(PadButton::Start));
assert_eq!(snapshot.axis(PadAxis::LeftTrigger), 0.5);
assert_eq!(snapshot.axis(PadAxis::RightY), -1.0, "down is -Y");
events.clear();
assert!(pads.poll(|event| events.push(event)).is_empty());
assert!(events.is_empty(), "no begin since: the pad stays");
__crcbl_web_pad_begin();
assert!(pads.poll(|event| events.push(event)).is_empty());
assert_eq!(events, [GamepadEvent::Disconnected { id }], "a begin alone");
}
}