//! gilrs → [`xenia_hid::GamepadState`] adapter. use gilrs::{Axis, Button, EventType, Gilrs, GilrsBuilder}; use xenia_hid::{GamepadState, buttons}; /// Simulated-pad policy: if no physical controller is attached, advertise /// a connected pad (with all-zero state) so games don't early-exit on /// `XamInputGetState → ERROR_DEVICE_NOT_CONNECTED`. Set /// `XENIA_FAKE_PAD=0` to disable and report the true no-controller status. fn fake_pad_policy() -> bool { match std::env::var("XENIA_FAKE_PAD").as_deref() { Ok("0") | Ok("false") | Ok("off") => false, _ => true, } } pub struct HostInput { ctx: Gilrs, /// Currently-active gilrs pad id. `None` until the first Connected event. active: Option, /// `true` if the pad is really physical; `false` for the zeroed simulated /// pad we advertise when no hardware is attached (see `fake_pad_policy`). pub physical_attached: bool, fake_allowed: bool, } impl HostInput { pub fn new() -> anyhow::Result { let ctx = GilrsBuilder::new() .add_included_mappings(true) .build() .map_err(|e| anyhow::anyhow!("gilrs init failed: {e}"))?; let active = ctx.gamepads().next().map(|(id, _)| id); let physical_attached = active.is_some(); let fake_allowed = fake_pad_policy(); tracing::info!( physical = physical_attached, fake_allowed, "input: init (pad simulation: {})", if fake_allowed && !physical_attached { "simulated (no physical pad)" } else if physical_attached { "physical" } else { "disabled" } ); Ok(Self { ctx, active, physical_attached, fake_allowed, }) } /// True iff a gamepad is "available" for user index 0 — either the /// actual host pad, or a simulated one when `XENIA_FAKE_PAD != 0`. pub fn is_connected(&self) -> bool { if self.physical_attached && self .active .and_then(|id| self.ctx.connected_gamepad(id)) .is_some() { return true; } self.fake_allowed } /// Pump gilrs events until empty. Updates the internal "active pad" /// registration; pad state is sampled by [`snapshot`]. pub fn poll(&mut self) { while let Some(event) = self.ctx.next_event() { match event.event { EventType::Connected => { if self.active.is_none() { self.active = Some(event.id); self.physical_attached = true; tracing::info!("gilrs: pad connected (id={:?})", event.id); } } EventType::Disconnected => { if self.active == Some(event.id) { self.active = None; tracing::info!("gilrs: pad disconnected (id={:?})", event.id); self.active = self.ctx.gamepads().next().map(|(id, _)| id); self.physical_attached = self.active.is_some(); } } _ => {} } } } /// Build an Xbox-360-layout `GamepadState` from the active gilrs pad. /// Returns `GamepadState::default()` (all-zero) when no pad is present. pub fn snapshot(&self) -> GamepadState { let Some(id) = self.active else { return GamepadState::default(); }; let Some(pad) = self.ctx.connected_gamepad(id) else { return GamepadState::default(); }; let mut state = GamepadState::default(); let pressed = |btn: Button| pad.is_pressed(btn); let set = |state: &mut GamepadState, btn: Button, mask: u16| { if pressed(btn) { state.buttons |= mask; } }; set(&mut state, Button::DPadUp, buttons::DPAD_UP); set(&mut state, Button::DPadDown, buttons::DPAD_DOWN); set(&mut state, Button::DPadLeft, buttons::DPAD_LEFT); set(&mut state, Button::DPadRight, buttons::DPAD_RIGHT); set(&mut state, Button::Start, buttons::START); set(&mut state, Button::Select, buttons::BACK); set(&mut state, Button::LeftThumb, buttons::LEFT_THUMB); set(&mut state, Button::RightThumb, buttons::RIGHT_THUMB); set(&mut state, Button::LeftTrigger, buttons::LEFT_SHOULDER); set(&mut state, Button::RightTrigger, buttons::RIGHT_SHOULDER); set(&mut state, Button::Mode, buttons::GUIDE); set(&mut state, Button::South, buttons::A); set(&mut state, Button::East, buttons::B); set(&mut state, Button::West, buttons::X); set(&mut state, Button::North, buttons::Y); // Triggers: gilrs reports LeftTrigger2 / RightTrigger2 as axes in // range [0.0, 1.0]. Map to u8 0..=255. let trig = |axis: Axis| -> u8 { pad.axis_data(axis) .map(|d| (d.value().clamp(0.0, 1.0) * 255.0) as u8) .unwrap_or(0) }; state.left_trigger = pad .button_data(Button::LeftTrigger2) .map(|d| (d.value().clamp(0.0, 1.0) * 255.0) as u8) .unwrap_or_else(|| trig(Axis::LeftZ)); state.right_trigger = pad .button_data(Button::RightTrigger2) .map(|d| (d.value().clamp(0.0, 1.0) * 255.0) as u8) .unwrap_or_else(|| trig(Axis::RightZ)); // Sticks: gilrs reports [-1.0, 1.0]. Xbox uses i16 [-32768, 32767]. let stick = |axis: Axis| -> i16 { let v = pad .axis_data(axis) .map(|d| d.value().clamp(-1.0, 1.0)) .unwrap_or(0.0); (v * 32767.0) as i16 }; state.left_stick_x = stick(Axis::LeftStickX); state.left_stick_y = stick(Axis::LeftStickY); state.right_stick_x = stick(Axis::RightStickX); state.right_stick_y = stick(Axis::RightStickY); state } }