Add gamepad support to the desktop build

Co-Authored-By: deepseek-v4.1-flash (opencode-go) via CodeCraft
This commit is contained in:
Richard Zinkanddeepseek-v4.1-flash (opencode-go) via CodeCraft committed 2026-11-11 15:43:32 +01:00
1 parent 8d51d6ec6a
commit e44aab6f5c
7 files changed
+260 -31

No files matched your search

+134 -12
View File
@@ -65,6 +65,32 @@ pub fn kind_for(setting: u8, detected: i32) -> Kind {
}
}
// Desktop: the pad's vendor id names the type (Microsoft, Sony, Nintendo)
#[cfg(not(target_arch = "wasm32"))]
fn kind_from_vendor(vendor: u16) -> i32 {
match vendor {
0x045e => 1,
0x054c => 2,
0x057e => 3,
_ => 0,
}
}
// Desktop: a pad without a known vendor, its name usually says what it is
#[cfg(not(target_arch = "wasm32"))]
fn kind_from_name(name: &str) -> i32 {
let n = name.to_lowercase();
if n.contains("xbox") || n.contains("xinput") || n.contains("microsoft") {
1
} else if n.contains("nintendo") || n.contains("switch") || n.contains("joy-con") || n.contains("joycon") || n.contains("pro controller") {
3
} else if n.contains("playstation") || n.contains("dualshock") || n.contains("dualsense") || n.contains("sony") || n.contains("wireless controller") {
2
} else {
0
}
}
thread_local! {
static KIND: Cell<Kind> = const { Cell::new(Kind::Xbox) };
}
@@ -84,28 +110,113 @@ pub struct Pad {
axes: [f32; 4],
pointer: (f32, f32), // device pixels of the interface
pointer_ready: bool,
// desktop builds read the pads themselves (miniquad does not poll them); gilrs opens the
// system's input devices, None when that is impossible or there are none
#[cfg(not(target_arch = "wasm32"))]
gilrs: Option<gilrs::Gilrs>,
#[cfg(not(target_arch = "wasm32"))]
tried: bool,
}
impl Pad {
pub fn new() -> Pad {
Pad { connected: false, buttons: 0, prev: 0, axes: [0.0; 4], pointer: (0.0, 0.0), pointer_ready: false }
Pad {
connected: false,
buttons: 0,
prev: 0,
axes: [0.0; 4],
pointer: (0.0, 0.0),
pointer_ready: false,
#[cfg(not(target_arch = "wasm32"))]
gilrs: None,
#[cfg(not(target_arch = "wasm32"))]
tried: false,
}
}
// Read the pad for this frame; kind_setting = Settings::gamepad.
pub fn poll(&mut self, kind_setting: u8) {
self.prev = if self.connected { self.buttons } else { 0 };
let slot = crate::platform::gamepad_slot();
self.connected = slot >= 0;
let detected = if self.connected { crate::platform::gamepad_kind(slot) } else { 0 };
KIND.with(|c| c.set(kind_for(kind_setting, detected)));
if !self.connected {
self.buttons = 0;
self.axes = [0.0; 4];
return;
#[cfg(target_arch = "wasm32")]
{
let slot = crate::platform::gamepad_slot();
self.connected = slot >= 0;
let detected = if self.connected { crate::platform::gamepad_kind(slot) } else { 0 };
KIND.with(|c| c.set(kind_for(kind_setting, detected)));
if !self.connected {
self.buttons = 0;
self.axes = [0.0; 4];
return;
}
self.buttons = crate::platform::gamepad_buttons(slot);
for (i, a) in self.axes.iter_mut().enumerate() {
*a = crate::platform::gamepad_axis(slot, i as i32);
}
}
self.buttons = crate::platform::gamepad_buttons(slot);
for (i, a) in self.axes.iter_mut().enumerate() {
*a = crate::platform::gamepad_axis(slot, i as i32);
#[cfg(not(target_arch = "wasm32"))]
self.poll_desktop(kind_setting);
}
// Desktop: gilrs reports the pad's buttons and sticks. Its Y axes point up when positive,
// the browser's Gamepad API points down, so they are flipped to the same meaning here.
#[cfg(not(target_arch = "wasm32"))]
fn poll_desktop(&mut self, kind_setting: u8) {
use gilrs::{Axis, Button as GBtn};
if self.gilrs.is_none() && !self.tried {
self.tried = true;
match gilrs::Gilrs::new() {
Ok(g) => self.gilrs = Some(g),
Err(e) => eprintln!("DungeonCraft: no gamepad support: {e}"),
}
}
let state = match self.gilrs.as_mut() {
None => None,
Some(g) => {
// drain the queue, so hot-plugged pads show up and the state stays current
while g.next_event().is_some() {}
g.gamepads().next().map(|(_, p)| {
let mut m = 0u32;
for (gb, b) in [
(GBtn::South, Btn::South),
(GBtn::East, Btn::East),
(GBtn::West, Btn::West),
(GBtn::North, Btn::North),
(GBtn::LeftTrigger, Btn::Lb),
(GBtn::RightTrigger, Btn::Rb),
(GBtn::LeftTrigger2, Btn::Lt),
(GBtn::RightTrigger2, Btn::Rt),
(GBtn::Select, Btn::Select),
(GBtn::Start, Btn::Start),
(GBtn::LeftThumb, Btn::LStick),
(GBtn::RightThumb, Btn::RStick),
(GBtn::DPadUp, Btn::Up),
(GBtn::DPadDown, Btn::Down),
(GBtn::DPadLeft, Btn::Left),
(GBtn::DPadRight, Btn::Right),
] {
if p.is_pressed(gb) {
m |= b.bit();
}
}
let axes = [p.value(Axis::LeftStickX), -p.value(Axis::LeftStickY), p.value(Axis::RightStickX), -p.value(Axis::RightStickY)];
let detected = p.vendor_id().map(kind_from_vendor).unwrap_or_else(|| kind_from_name(&p.name()));
(detected, m, axes)
})
}
};
match state {
Some((detected, buttons, axes)) => {
self.connected = true;
self.buttons = buttons;
self.axes = axes;
KIND.with(|c| c.set(kind_for(kind_setting, detected)));
}
None => {
self.connected = false;
self.buttons = 0;
self.axes = [0.0; 4];
KIND.with(|c| c.set(kind_for(kind_setting, 0)));
}
}
}
@@ -212,6 +323,17 @@ mod tests {
assert!(!p.pressed(Btn::South));
}
// the desktop path opens the system's input devices; without a pad (or without access to
// them) it simply reports none, and it must never panic
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn polling_the_desktop_pad_is_safe() {
let mut p = Pad::new();
p.poll(0);
p.poll(2);
assert_eq!(p.connected(), p.gilrs.as_ref().map_or(false, |g| g.gamepads().next().is_some()));
}
#[test]
fn the_pointer_follows_the_mouse_and_the_stick() {
let mut p = Pad::new();
+2 -14
View File
@@ -388,20 +388,8 @@ mod imp {
}
}
// The Gamepad API only exists in browsers. The desktop build has no gamepad input yet
// (miniquad does not poll gamepads; that would need a crate like gilrs).
pub fn gamepad_slot() -> i32 {
-1
}
pub fn gamepad_buttons(_slot: i32) -> u32 {
0
}
pub fn gamepad_axis(_slot: i32, _axis: i32) -> f32 {
0.0
}
pub fn gamepad_kind(_slot: i32) -> i32 {
0
}
// Gamepads are polled by pad.rs with gilrs; the browser's Gamepad API lives in the
// dc_gamepad_* imports of the JS plugin (see the wasm imp above).
}
pub use imp::*;