Add gamepad support to the desktop build
Co-Authored-By: deepseek-v4.1-flash (opencode-go) via CodeCraft
This commit is contained in:
1 parent
8d51d6ec6a
commit
e44aab6f5c
7 files changed
+260
-31
No files matched your search
Generated
+116
@@ -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
@@ -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
@@ -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
|
||||
|
||||
+134
-12
@@ -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
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user