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
+249 -20

No files matched your search

Generated
+116
View File
@@ -93,6 +93,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -194,6 +200,7 @@ name = "dungeoncraft"
version = "0.1.0"
dependencies = [
"cpal",
"gilrs",
"macroquad",
"rfd",
"serde",
@@ -226,6 +233,12 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
@@ -266,6 +279,40 @@ dependencies = [
"slab",
]
[[package]]
name = "gilrs"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "902fb00d3f6398e635be22e5c837b303c501835cca7ac11a47bba138f7aafdd8"
dependencies = [
"fnv",
"gilrs-core",
"log",
"uuid",
"vec_map",
]
[[package]]
name = "gilrs-core"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc7f0ce6237abcc0523f2a5502b1e3fe5802daaae47ac14e166fe49551301ea9"
dependencies = [
"inotify",
"js-sys",
"libc",
"libudev-sys",
"log",
"nix",
"objc2-core-foundation",
"objc2-io-kit",
"uuid",
"vec_map",
"wasm-bindgen",
"web-sys",
"windows",
]
[[package]]
name = "glam"
version = "0.27.0"
@@ -312,6 +359,26 @@ dependencies = [
"hashbrown 0.17.1",
]
[[package]]
name = "inotify"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592"
dependencies = [
"bitflags 2.13.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
dependencies = [
"libc",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -399,6 +466,16 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libudev-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "log"
version = "0.4.35"
@@ -513,6 +590,18 @@ dependencies = [
"jni-sys 0.3.1",
]
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "num-derive"
version = "0.4.2"
@@ -666,6 +755,17 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"bitflags 2.13.2",
"libc",
"objc2-core-foundation",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -983,6 +1083,22 @@ version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "uuid"
version = "1.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bed99139f7dac9b5cfe5f17be4d66dc7079a34dee1d74bcc77217119240e267c"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "walkdir"
version = "2.5.0"
+2 -1
View File
@@ -26,7 +26,8 @@ inherits = "release"
lto = "thin"
codegen-units = 16
# desktop builds: system file dialog (xdg portal on Linux) and sound output
# desktop builds: system file dialog (xdg portal on Linux), sound output and gamepads
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rfd = { version = "0.17", default-features = false, features = ["xdg-portal"] }
cpal = "0.18"
gilrs = "0.11"
+3 -2
View File
@@ -46,8 +46,9 @@ cargo build --release
./target/release/dungeoncraft
```
On Linux the build needs the ALSA headers (`alsa-lib` / `libasound2-dev`); the sound goes through ALSA (PipeWire and
PulseAudio take it). The settings, the autosave, the campaign progress and the profile are kept as files in the
On Linux the build needs the ALSA headers (`alsa-lib` / `libasound2-dev`) and libudev for gamepads
(`libudev-dev`); the sound goes through ALSA (PipeWire and PulseAudio take it). Gamepads are read directly on the
desktop (gilrs, hot-plug included); the browser build uses the browser's Gamepad API. The settings, the autosave, the campaign progress and the profile are kept as files in the
data folder: `~/.local/share/dungeoncraft/` (Linux, or `$XDG_DATA_HOME/dungeoncraft`), `%APPDATA%\DungeonCraft`
(Windows), `~/Library/Application Support/DungeonCraft` (macOS); `DUNGEONCRAFT_DATA` chooses another one.
*Save Game* and saved campaigns land in its `saves/` folder; *Load Game* opens the system file dialog there (on
+1 -1
View File
@@ -1 +1 @@
v202611-00079
v202611-00080
+123 -1
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,16 +110,35 @@ 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 };
#[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 };
@@ -108,6 +153,72 @@ impl Pad {
*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)));
}
}
}
pub fn connected(&self) -> bool {
self.connected
@@ -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::*;
+2 -1
View File
@@ -6,7 +6,8 @@
- linux: dist/DungeonCraft-<version>-x86_64.AppImage
built with cargo-zigbuild against glibc 2.28, so it runs on distributions from about 2019 on;
needs on the build machine: zig, cargo-zigbuild, the ALSA headers (alsa-lib / libasound2-dev);
needs on the build machine: zig, cargo-zigbuild, the ALSA headers (alsa-lib / libasound2-dev)
and libudev (libudev-dev, for gamepads);
appimagetool is downloaded once into ~/.cache/dungeoncraft/
- windows: dist/DungeonCraft-<version>-windows-x64.exe (one file, Windows 10 and newer)
cross-compiled with cargo-zigbuild for x86_64-pc-windows-gnu