mod ffi;
use crate::{GamepadEvent, GamepadId, GamepadSnapshot, PadAxis, PadButton, PadKind};
use ffi::{ERROR_DEVICE_NOT_CONNECTED, XInputGamepad, XInputState, XUSER_MAX_COUNT};
pub const VALVE_VENDOR_ID: u16 = 0x28DE;
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum XInputError {
Unavailable {
code: u32,
},
GetState {
user: u32,
code: u32,
},
NoVendorQuery,
GetVendor {
user: u32,
code: u32,
},
}
impl std::fmt::Display for XInputError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unavailable { code } => {
write!(f, "no XInput library with XInputGetState (error {code})")
}
Self::GetState { user, code } => {
write!(f, "XInputGetState failed for user {user} (error {code})")
}
Self::NoVendorQuery => write!(
f,
"this XInput library cannot report vendor ids, so Steam's virtual pads \
cannot be skipped"
),
Self::GetVendor { user, code } => write!(
f,
"XInputGetCapabilitiesEx failed for user {user} (error {code}); \
the pad is reported"
),
}
}
}
impl std::error::Error for XInputError {}
#[must_use]
pub fn stick_axis(value: i16) -> f32 {
(f32::from(value) / f32::from(i16::MAX)).max(-1.0)
}
#[must_use]
pub fn trigger_axis(value: u8) -> f32 {
f32::from(value) / f32::from(u8::MAX)
}
const BUTTONS: [(u16, PadButton); 14] = [
(ffi::XINPUT_GAMEPAD_A, PadButton::South),
(ffi::XINPUT_GAMEPAD_B, PadButton::East),
(ffi::XINPUT_GAMEPAD_X, PadButton::West),
(ffi::XINPUT_GAMEPAD_Y, PadButton::North),
(ffi::XINPUT_GAMEPAD_LEFT_SHOULDER, PadButton::LeftShoulder),
(ffi::XINPUT_GAMEPAD_RIGHT_SHOULDER, PadButton::RightShoulder),
(ffi::XINPUT_GAMEPAD_LEFT_THUMB, PadButton::LeftStick),
(ffi::XINPUT_GAMEPAD_RIGHT_THUMB, PadButton::RightStick),
(ffi::XINPUT_GAMEPAD_START, PadButton::Start),
(ffi::XINPUT_GAMEPAD_BACK, PadButton::Select),
(ffi::XINPUT_GAMEPAD_DPAD_UP, PadButton::DpadUp),
(ffi::XINPUT_GAMEPAD_DPAD_DOWN, PadButton::DpadDown),
(ffi::XINPUT_GAMEPAD_DPAD_LEFT, PadButton::DpadLeft),
(ffi::XINPUT_GAMEPAD_DPAD_RIGHT, PadButton::DpadRight),
];
fn snapshot_of(pad: &XInputGamepad) -> GamepadSnapshot {
let mut snapshot = GamepadSnapshot::neutral(PadKind::Xbox);
snapshot.buttons = BUTTONS
.iter()
.filter(|(bit, _)| pad.buttons & bit != 0)
.map(|&(_, button)| button)
.collect();
snapshot.axes[PadAxis::LeftX as usize] = stick_axis(pad.thumb_lx);
snapshot.axes[PadAxis::LeftY as usize] = stick_axis(pad.thumb_ly);
snapshot.axes[PadAxis::RightX as usize] = stick_axis(pad.thumb_rx);
snapshot.axes[PadAxis::RightY as usize] = stick_axis(pad.thumb_ry);
snapshot.axes[PadAxis::LeftTrigger as usize] = trigger_axis(pad.left_trigger);
snapshot.axes[PadAxis::RightTrigger as usize] = trigger_axis(pad.right_trigger);
snapshot
}
trait StateSource {
fn get_state(&mut self, user: u32) -> Result<XInputState, u32>;
fn vendor(&mut self, user: u32) -> Option<Result<u16, u32>>;
}
#[derive(Clone, Copy, Debug)]
enum Slot {
Reported {
id: GamepadId,
last: GamepadSnapshot,
vendor_checked: bool,
},
Skipped,
}
pub const REPROBE_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Debug, Default)]
struct Poller {
slots: [Option<Slot>; XUSER_MAX_COUNT as usize],
found_empty: [Option<Instant>; XUSER_MAX_COUNT as usize],
skip_valve: bool,
}
impl Poller {
fn set_skip_valve(&mut self, skip: bool) {
self.skip_valve = skip;
for slot in &mut self.slots {
match slot {
Some(Slot::Reported { vendor_checked, .. }) => *vendor_checked = false,
Some(Slot::Skipped) if !skip => *slot = None,
_ => {}
}
}
}
fn poll(
&mut self,
source: &mut impl StateSource,
now: Instant,
emit: &mut impl FnMut(GamepadEvent),
) -> Result<(), XInputError> {
let mut failure = None;
let skip_valve = self.skip_valve;
let slots = self.slots.iter_mut().zip(&mut self.found_empty);
for (user, (slot, found_empty)) in (0..XUSER_MAX_COUNT).zip(slots) {
if found_empty.is_some_and(|at| now.duration_since(at) < REPROBE_INTERVAL) {
continue;
}
match source.get_state(user) {
Ok(state) => {
let unchecked = !matches!(
slot,
Some(
Slot::Skipped
| Slot::Reported {
vendor_checked: true,
..
}
)
);
if skip_valve && unchecked {
match source.vendor(user) {
Some(Ok(VALVE_VENDOR_ID)) => {
if let Some(Slot::Reported { id, .. }) = slot.take() {
emit(GamepadEvent::Disconnected { id });
}
*slot = Some(Slot::Skipped);
continue;
}
Some(Err(code)) => {
failure = Some(XInputError::GetVendor { user, code });
}
Some(Ok(_)) | None => {}
}
}
report(slot, snapshot_of(&state.gamepad), skip_valve, emit);
}
Err(code) => {
if let Some(Slot::Reported { id, .. }) = slot.take() {
emit(GamepadEvent::Disconnected { id });
}
if code == ERROR_DEVICE_NOT_CONNECTED {
*found_empty = Some(now);
} else {
failure = Some(XInputError::GetState { user, code });
}
}
}
}
failure.map_or(Ok(()), Err)
}
}
fn report(
slot: &mut Option<Slot>,
snapshot: GamepadSnapshot,
checked: bool,
emit: &mut impl FnMut(GamepadEvent),
) {
let reported = slot.get_or_insert_with(|| {
let id = GamepadId::allocate();
emit(GamepadEvent::Connected {
id,
kind: PadKind::Xbox,
});
Slot::Reported {
id,
last: GamepadSnapshot::neutral(PadKind::Xbox),
vendor_checked: checked,
}
});
if let Slot::Reported {
id,
last,
vendor_checked,
} = reported
{
*vendor_checked |= checked;
if *last != snapshot {
*last = snapshot;
emit(GamepadEvent::State { id: *id, snapshot });
}
}
}
#[cfg(windows)]
pub use loaded::XInput;
#[cfg(windows)]
mod loaded {
use super::ffi::{
ERROR_SUCCESS, FreeLibrary, GET_CAPABILITIES_EX_ORDINAL, GetLastError, GetProcAddress,
LoadLibraryW, Module, XInputCapabilitiesEx, XInputGetCapabilitiesExFn, XInputGetStateFn,
XInputState,
};
use super::{Poller, StateSource, XInputError};
use crate::GamepadEvent;
use std::time::Instant;
const LIBRARIES: [&str; 2] = ["xinput1_4.dll", "xinput9_1_0.dll"];
#[derive(Debug)]
pub(super) struct Library {
module: Module,
get_state: XInputGetStateFn,
get_capabilities_ex: Option<XInputGetCapabilitiesExFn>,
name: &'static str,
}
impl Library {
pub(super) fn load() -> Result<Self, XInputError> {
let mut code = 0;
for name in LIBRARIES {
let wide: Vec<u16> = name.encode_utf16().chain([0]).collect();
let module = unsafe { LoadLibraryW(wide.as_ptr()) };
if module.is_null() {
code = unsafe { GetLastError() };
continue;
}
let proc = unsafe { GetProcAddress(module, c"XInputGetState".as_ptr().cast()) };
if proc.is_null() {
unsafe {
code = GetLastError();
FreeLibrary(module);
}
continue;
}
let get_state = unsafe {
core::mem::transmute::<*mut core::ffi::c_void, XInputGetStateFn>(proc)
};
let ex = unsafe {
GetProcAddress(
module,
core::ptr::without_provenance(GET_CAPABILITIES_EX_ORDINAL),
)
};
let get_capabilities_ex = (!ex.is_null()).then(|| unsafe {
core::mem::transmute::<*mut core::ffi::c_void, XInputGetCapabilitiesExFn>(ex)
});
return Ok(Self {
module,
get_state,
get_capabilities_ex,
name,
});
}
Err(XInputError::Unavailable { code })
}
}
impl StateSource for Library {
fn get_state(&mut self, user: u32) -> Result<XInputState, u32> {
let mut state = XInputState::default();
let code = unsafe { (self.get_state)(user, &raw mut state) };
if code == ERROR_SUCCESS {
Ok(state)
} else {
Err(code)
}
}
fn vendor(&mut self, user: u32) -> Option<Result<u16, u32>> {
let get_capabilities_ex = self.get_capabilities_ex?;
let mut capabilities = XInputCapabilitiesEx::default();
let code = unsafe { get_capabilities_ex(1, user, 0, &raw mut capabilities) };
Some(if code == ERROR_SUCCESS {
Ok(capabilities.vendor_id)
} else {
Err(code)
})
}
}
impl Drop for Library {
fn drop(&mut self) {
let freed = unsafe { FreeLibrary(self.module) };
debug_assert_ne!(freed, 0, "FreeLibrary refused {}", self.name);
}
}
#[derive(Debug)]
pub struct XInput {
library: Library,
poller: Poller,
}
impl XInput {
pub fn load() -> Result<Self, XInputError> {
Ok(Self {
library: Library::load()?,
poller: Poller::default(),
})
}
#[must_use]
pub fn library_name(&self) -> &'static str {
self.library.name
}
pub fn skip_steam_virtual_pads(&mut self, skip: bool) -> Result<(), XInputError> {
if skip && self.library.get_capabilities_ex.is_none() {
return Err(XInputError::NoVendorQuery);
}
self.poller.set_skip_valve(skip);
Ok(())
}
pub fn poll(&mut self, mut emit: impl FnMut(GamepadEvent)) -> Result<(), XInputError> {
self.poller
.poll(&mut self.library, Instant::now(), &mut emit)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ActionDecl, ActionKind, ActionMap, Binding, PadButtons};
#[test]
fn stick_and_trigger_values_normalise_against_known_codes() {
assert_eq!(stick_axis(-32768), -1.0, "the spare code clamps");
assert_eq!(stick_axis(-32767), -1.0);
assert_eq!(stick_axis(0), 0.0);
assert_eq!(stick_axis(32767), 1.0);
assert_eq!(
stick_axis(-16384),
-stick_axis(16384),
"one scale both ways"
);
assert_eq!(trigger_axis(0), 0.0);
assert_eq!(trigger_axis(255), 1.0);
assert!((trigger_axis(128) - 128.0 / 255.0).abs() < f32::EPSILON);
}
#[test]
fn the_raw_state_maps_positionally_with_y_up() {
for &(bit, button) in &BUTTONS {
let snapshot = snapshot_of(&XInputGamepad {
buttons: bit,
..XInputGamepad::default()
});
assert_eq!(
snapshot.buttons,
[button].into_iter().collect::<PadButtons>(),
"bit {bit:#06x}"
);
}
let snapshot = snapshot_of(&XInputGamepad {
thumb_ly: i16::MAX,
thumb_rx: i16::MIN,
right_trigger: u8::MAX,
..XInputGamepad::default()
});
assert_eq!(snapshot.axis(PadAxis::LeftY), 1.0, "pushed up is +Y");
assert_eq!(snapshot.axis(PadAxis::RightX), -1.0);
assert_eq!(snapshot.axis(PadAxis::RightTrigger), 1.0);
assert_eq!(snapshot.kind, PadKind::Xbox);
}
#[derive(Debug)]
struct Fake {
slots: [Result<XInputState, u32>; 4],
reads: [u32; 4],
vendors: [Option<Result<u16, u32>>; 4],
vendor_reads: [u32; 4],
now: Instant,
}
impl Fake {
fn new(slots: [Result<XInputState, u32>; 4]) -> Self {
Self {
slots,
reads: [0; 4],
vendors: [Some(Ok(MICROSOFT)); 4],
vendor_reads: [0; 4],
now: Instant::now(),
}
}
}
impl StateSource for Fake {
fn get_state(&mut self, user: u32) -> Result<XInputState, u32> {
self.reads[user as usize] += 1;
self.slots[user as usize]
}
fn vendor(&mut self, user: u32) -> Option<Result<u16, u32>> {
self.vendor_reads[user as usize] += 1;
self.vendors[user as usize]
}
}
const MICROSOFT: u16 = 0x045E;
const EMPTY: Result<XInputState, u32> = Err(ERROR_DEVICE_NOT_CONNECTED);
fn pad(buttons: u16) -> Result<XInputState, u32> {
Ok(XInputState {
packet_number: 0,
gamepad: XInputGamepad {
buttons,
..XInputGamepad::default()
},
})
}
fn poll_after(
poller: &mut Poller,
fake: &mut Fake,
after: Duration,
) -> (Vec<GamepadEvent>, Result<(), XInputError>) {
fake.now += after;
let now = fake.now;
let mut events = Vec::new();
let result = poller.poll(fake, now, &mut |event| events.push(event));
(events, result)
}
fn poll(poller: &mut Poller, fake: &mut Fake) -> (Vec<GamepadEvent>, Result<(), XInputError>) {
poll_after(poller, fake, REPROBE_INTERVAL)
}
#[test]
fn an_empty_slot_is_reprobed_only_after_the_interval() {
let mut poller = Poller::default();
let mut fake = Fake::new([EMPTY; 4]);
poll(&mut poller, &mut fake).1.expect("scripted");
assert_eq!(fake.reads, [1; 4], "the first poll asks every slot");
let almost = REPROBE_INTERVAL - Duration::from_millis(1);
poll_after(&mut poller, &mut fake, Duration::ZERO)
.1
.expect("scripted");
poll_after(&mut poller, &mut fake, almost)
.1
.expect("scripted");
assert_eq!(fake.reads, [1; 4], "nothing re-asked inside the interval");
poll_after(&mut poller, &mut fake, Duration::from_millis(1))
.1
.expect("scripted");
assert_eq!(fake.reads, [2; 4], "every slot re-asked once it passed");
}
#[test]
fn a_connected_slot_is_read_every_poll() {
let mut poller = Poller::default();
let mut fake = Fake::new([EMPTY, pad(0), EMPTY, EMPTY]);
poll(&mut poller, &mut fake).1.expect("scripted");
for _ in 0..3 {
poll_after(&mut poller, &mut fake, Duration::ZERO)
.1
.expect("scripted");
}
assert_eq!(fake.reads, [1, 4, 1, 1]);
fake.slots[1] = pad(ffi::XINPUT_GAMEPAD_A);
let (events, _) = poll_after(&mut poller, &mut fake, Duration::ZERO);
assert!(
matches!(events[..], [GamepadEvent::State { .. }]),
"a press on the connected pad arrives at once: {events:?}"
);
}
#[test]
fn a_new_pad_is_detected_within_one_interval() {
let mut poller = Poller::default();
let mut fake = Fake::new([EMPTY; 4]);
poll(&mut poller, &mut fake).1.expect("scripted");
let half = REPROBE_INTERVAL / 2;
let (events, _) = poll_after(&mut poller, &mut fake, half);
assert!(events.is_empty(), "{events:?}");
fake.slots[2] = pad(0);
let (events, _) = poll_after(&mut poller, &mut fake, half - Duration::from_millis(1));
assert!(events.is_empty(), "still inside the interval: {events:?}");
let (events, _) = poll_after(&mut poller, &mut fake, Duration::from_millis(1));
assert!(
matches!(events[..], [GamepadEvent::Connected { .. }]),
"{events:?}"
);
}
#[test]
fn an_unplugged_slot_is_reprobed_after_the_interval() {
let mut poller = Poller::default();
let mut fake = Fake::new([pad(0), EMPTY, EMPTY, EMPTY]);
poll(&mut poller, &mut fake).1.expect("scripted");
fake.slots[0] = EMPTY;
let (events, _) = poll_after(&mut poller, &mut fake, Duration::ZERO);
assert!(matches!(events[..], [GamepadEvent::Disconnected { .. }]));
fake.slots[0] = pad(0);
let (events, _) = poll_after(&mut poller, &mut fake, Duration::ZERO);
assert!(events.is_empty(), "inside the interval: {events:?}");
let (events, _) = poll(&mut poller, &mut fake);
assert!(matches!(events[..], [GamepadEvent::Connected { .. }]));
}
#[test]
fn slot_transitions_are_reported_once_each() {
let mut poller = Poller::default();
let mut fake = Fake::new([EMPTY; 4]);
assert_eq!(poll(&mut poller, &mut fake), (vec![], Ok(())), "all empty");
fake.slots[1] = pad(ffi::XINPUT_GAMEPAD_A);
let (events, result) = poll(&mut poller, &mut fake);
assert_eq!(result, Ok(()));
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::Xbox, id));
assert!(snapshot.buttons.contains(PadButton::South));
assert_eq!(poll(&mut poller, &mut fake), (vec![], Ok(())), "unchanged");
fake.slots[1] = pad(0);
let (events, _) = poll(&mut poller, &mut fake);
assert_eq!(
events,
[GamepadEvent::State {
id,
snapshot: GamepadSnapshot::neutral(PadKind::Xbox),
}],
);
fake.slots[1] = EMPTY;
assert_eq!(
poll(&mut poller, &mut fake),
(vec![GamepadEvent::Disconnected { id }], Ok(())),
);
fake.slots[1] = pad(0);
let (events, _) = poll(&mut poller, &mut fake);
let [GamepadEvent::Connected { id: again, .. }] = events[..] else {
panic!("a reconnection at rest is one event: {events:?}");
};
assert_ne!(again, id, "XInput names a slot, not a pad");
}
#[test]
fn an_unexpected_error_disconnects_the_slot_and_is_reported() {
let mut poller = Poller::default();
let mut fake = Fake::new([pad(0), EMPTY, EMPTY, EMPTY]);
let (events, _) = poll(&mut poller, &mut fake);
let [GamepadEvent::Connected { id, .. }] = events[..] else {
panic!("{events:?}");
};
fake.slots[0] = Err(5);
fake.slots[3] = pad(ffi::XINPUT_GAMEPAD_B);
let (events, result) = poll(&mut poller, &mut fake);
assert_eq!(result, Err(XInputError::GetState { user: 0, code: 5 }));
assert_eq!(events[0], GamepadEvent::Disconnected { id });
assert!(
matches!(events[1], GamepadEvent::Connected { .. }),
"slot 3 was still polled: {events:?}"
);
}
#[test]
fn polled_events_feed_a_game_adapter_with_no_action_map() {
#[derive(Default)]
struct Adapter {
device: Option<u64>,
left: (f32, f32),
}
impl Adapter {
fn feed(&mut self, event: GamepadEvent) {
match event {
GamepadEvent::State { id, snapshot } => {
assert!(snapshot.is_finite());
self.device = Some(u64::from(id.0));
self.left = snapshot.stick(crate::Stick::Left);
}
GamepadEvent::Disconnected { id } if self.device == Some(u64::from(id.0)) => {
*self = Self::default();
}
_ => {}
}
}
}
let mut adapter = Adapter::default();
let mut poller = Poller::default();
let mut fake = Fake::new([EMPTY; 4]);
fake.slots[0] = Ok(XInputState {
packet_number: 1,
gamepad: XInputGamepad {
thumb_lx: 3277,
thumb_ly: i16::MAX,
..XInputGamepad::default()
},
});
poller
.poll(&mut fake, Instant::now(), &mut |event| adapter.feed(event))
.expect("scripted");
assert_eq!(adapter.left, (stick_axis(3277), 1.0), "raw, and +Y up");
let device = adapter.device.expect("a state arrived");
fake.slots[0] = EMPTY;
poller
.poll(&mut fake, Instant::now(), &mut |event| adapter.feed(event))
.expect("scripted");
assert_eq!(adapter.device, None, "pad {device} unplugged");
assert_eq!(adapter.left, (0.0, 0.0));
}
#[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::new([EMPTY, EMPTY, pad(ffi::XINPUT_GAMEPAD_A), EMPTY]);
poller
.poll(&mut fake, Instant::now(), &mut |event| {
map.gamepad_event(&event)
})
.expect("scripted");
assert!(map.just_pressed("jump"));
fake.slots[2] = EMPTY;
poller
.poll(&mut fake, Instant::now(), &mut |event| {
map.gamepad_event(&event)
})
.expect("scripted");
assert!(map.just_released("jump"), "unplugging releases");
}
#[test]
fn with_the_filter_on_steams_virtual_pad_is_never_reported() {
let mut poller = Poller::default();
poller.set_skip_valve(true);
let mut fake = Fake::new([pad(0), pad(ffi::XINPUT_GAMEPAD_A), EMPTY, EMPTY]);
fake.vendors[1] = Some(Ok(VALVE_VENDOR_ID));
let (events, result) = poll(&mut poller, &mut fake);
assert_eq!(result, Ok(()));
assert!(
matches!(events[..], [GamepadEvent::Connected { .. }]),
"only slot 0, at rest: {events:?}"
);
fake.slots[1] = pad(ffi::XINPUT_GAMEPAD_B);
assert_eq!(poll(&mut poller, &mut fake), (vec![], Ok(())));
fake.slots[1] = EMPTY;
assert_eq!(
poll(&mut poller, &mut fake),
(vec![], Ok(())),
"and leaves unannounced"
);
assert_eq!(fake.vendor_reads, [1, 1, 0, 0], "asked once per pad");
}
#[test]
fn toggling_the_filter_hands_a_steam_pad_over_and_back() {
let mut poller = Poller::default();
let mut fake = Fake::new([pad(0), EMPTY, EMPTY, pad(0)]);
fake.vendors[3] = Some(Ok(VALVE_VENDOR_ID));
let (events, _) = poll(&mut poller, &mut fake);
let [
GamepadEvent::Connected { .. },
GamepadEvent::Connected { id: steam, .. },
] = events[..]
else {
panic!("both reported with the filter off: {events:?}");
};
assert_eq!(fake.vendor_reads, [0; 4], "no filter, no query");
poller.set_skip_valve(true);
let (events, _) = poll(&mut poller, &mut fake);
assert_eq!(events, [GamepadEvent::Disconnected { id: steam }]);
assert_eq!(poll(&mut poller, &mut fake), (vec![], Ok(())));
poller.set_skip_valve(false);
let (events, _) = poll(&mut poller, &mut fake);
let [GamepadEvent::Connected { id: again, .. }] = events[..] else {
panic!("the Steam pad comes back: {events:?}");
};
assert_ne!(again, steam);
}
#[test]
fn a_failed_vendor_query_reports_the_pad_and_the_error_once() {
let mut poller = Poller::default();
poller.set_skip_valve(true);
let mut fake = Fake::new([EMPTY, EMPTY, pad(0), EMPTY]);
fake.vendors[2] = Some(Err(5));
let (events, result) = poll(&mut poller, &mut fake);
assert_eq!(result, Err(XInputError::GetVendor { user: 2, code: 5 }));
assert!(matches!(events[..], [GamepadEvent::Connected { .. }]));
assert_eq!(poll(&mut poller, &mut fake), (vec![], Ok(())));
assert_eq!(fake.vendor_reads, [0, 0, 1, 0]);
}
#[cfg(windows)]
#[test]
fn the_real_library_answers_the_vendor_query_like_the_state_query() {
let mut library = loaded::Library::load().expect("XInput ships with Windows");
for user in 0..XUSER_MAX_COUNT {
let state = library.get_state(user).map(|_| ());
let vendor = library
.vendor(user)
.expect("xinput1_4.dll exports ordinal 108");
match state {
Ok(()) => assert!(vendor.is_ok(), "user {user}: {vendor:?}"),
Err(code) => assert_eq!(vendor, Err(code), "user {user}"),
}
}
}
#[cfg(windows)]
#[test]
fn the_real_library_answers_an_empty_slot_as_not_connected() {
let mut library = loaded::Library::load().expect("XInput ships with Windows");
let answers: Vec<Result<(), u32>> = (0..XUSER_MAX_COUNT)
.map(|user| library.get_state(user).map(|_| ()))
.collect();
assert!(
answers
.iter()
.all(|answer| matches!(answer, Ok(()) | Err(ERROR_DEVICE_NOT_CONNECTED))),
"{answers:?}",
);
let empty = answers.iter().filter(|answer| answer.is_err()).count();
assert!(empty > 0, "every slot has a pad, so none could answer 1167");
let mut events = Vec::new();
Poller::default()
.poll(&mut library, Instant::now(), &mut |event| {
events.push(event)
})
.expect("1167 is an empty slot, not an error");
let connected = events
.iter()
.filter(|event| matches!(event, GamepadEvent::Connected { .. }))
.count();
assert_eq!(connected, answers.len() - empty);
}
}