desktop: Add gamepad support using gilrs

This commit is contained in:
Tom Schuster 2024-01-01 01:50:34 +01:00
parent d07b154898
commit a03355458f
6 changed files with 227 additions and 6 deletions

98
Cargo.lock generated
View File

@ -2357,6 +2357,40 @@ dependencies = [
"weezl",
]
[[package]]
name = "gilrs"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b2e57a9cb946b5d04ae8638c5f554abb5a9f82c4c950fd5b1fee6d119592fb"
dependencies = [
"fnv",
"gilrs-core",
"log",
"uuid",
"vec_map",
]
[[package]]
name = "gilrs-core"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0af1827b7dd2f36d740ae804c1b3ea0d64c12533fb61ff91883005143a0e8c5a"
dependencies = [
"core-foundation",
"inotify",
"io-kit-sys",
"js-sys",
"libc",
"libudev-sys",
"log",
"nix 0.27.1",
"uuid",
"vec_map",
"wasm-bindgen",
"web-sys",
"windows 0.52.0",
]
[[package]]
name = "gimli"
version = "0.28.1"
@ -2733,6 +2767,26 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "inotify"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "insta"
version = "1.35.1"
@ -2774,6 +2828,16 @@ dependencies = [
"unic-langid",
]
[[package]]
name = "io-kit-sys"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4769cb30e5dcf1710fc6730d3e94f78c47723a014a567de385e113c737394640"
dependencies = [
"core-foundation-sys",
"mach2",
]
[[package]]
name = "isahc"
version = "1.7.2"
@ -3040,6 +3104,16 @@ dependencies = [
"threadpool",
]
[[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 = "libz-sys"
version = "1.1.15"
@ -3498,6 +3572,17 @@ dependencies = [
"pin-utils",
]
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.28.0"
@ -4467,6 +4552,7 @@ dependencies = [
"futures",
"futures-lite 2.2.0",
"generational-arena",
"gilrs",
"image",
"isahc",
"macro_rules_attribute",
@ -5900,6 +5986,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
[[package]]
name = "valuable"
version = "0.1.0"
@ -5912,6 +6004,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "vergen"
version = "8.3.1"

View File

@ -46,6 +46,7 @@ futures-lite = "2.2.0"
async-io = "2.3.1"
async-net = "2.0.0"
async-channel = "2.2.0"
gilrs = "0.10"
# Deliberately held back to match tracy client used by profiling crate
tracing-tracy = { version = "=0.10.4", optional = true }

View File

@ -3,10 +3,11 @@ use crate::custom_event::RuffleEvent;
use crate::gui::{GuiController, MENU_HEIGHT};
use crate::player::{PlayerController, PlayerOptions};
use crate::util::{
get_screen_size, parse_url, pick_file, plot_stats_in_tracy, winit_to_ruffle_key_code,
winit_to_ruffle_text_control,
get_screen_size, gilrs_button_to_gamepad_button, parse_url, pick_file, plot_stats_in_tracy,
winit_to_ruffle_key_code, winit_to_ruffle_text_control,
};
use anyhow::{Context, Error};
use gilrs::{Event, EventType, Gilrs};
use ruffle_core::{PlayerEvent, StageDisplayState};
use ruffle_render::backend::ViewportDimensions;
use std::cell::RefCell;
@ -99,6 +100,12 @@ impl App {
loaded = LoadingState::Loaded;
}
let mut gilrs = Gilrs::new()
.inspect_err(|err| {
tracing::warn!("Gamepad support could not be initialized: {err}");
})
.ok();
// Poll UI events.
let event_loop = self.event_loop.take().expect("App already running");
event_loop.run(move |event, elwt| {
@ -449,6 +456,26 @@ impl App {
_ => (),
}
if let Some(Event { event, .. }) = gilrs.as_mut().and_then(|gilrs| gilrs.next_event()) {
match event {
EventType::ButtonPressed(button, _) => {
if let Some(button) = gilrs_button_to_gamepad_button(button) {
self.player
.handle_event(PlayerEvent::GamepadButtonDown { button });
check_redraw = true;
}
}
EventType::ButtonReleased(button, _) => {
if let Some(button) = gilrs_button_to_gamepad_button(button) {
self.player
.handle_event(PlayerEvent::GamepadButtonUp { button });
check_redraw = true;
}
}
_ => {}
}
}
// Check for a redraw request.
if check_redraw {
let player = self.player.get();

View File

@ -1,8 +1,9 @@
use crate::RUFFLE_VERSION;
use anyhow::Error;
use clap::Parser;
use anyhow::{anyhow, Error};
use clap::{Parser, ValueEnum};
use ruffle_core::backend::navigator::{OpenURLMode, SocketMode};
use ruffle_core::config::Letterbox;
use ruffle_core::events::{GamepadButton, KeyCode};
use ruffle_core::{LoadBehavior, PlayerRuntime, StageAlign, StageScaleMode};
use ruffle_render::quality::StageQuality;
use ruffle_render_wgpu::clap::{GraphicsBackend, PowerPreference};
@ -140,12 +141,76 @@ pub struct Opt {
/// Hides the menu bar (the bar at the top of the window).
#[clap(long)]
pub no_gui: bool,
/// Remaps a specific button on a gamepad to a keyboard key.
/// This can be used to add new gamepad support to existing games, for example mapping
/// the D-pad to the arrow keys with -B d-pad-up=up -B d-pad-down=down etc.
///
/// A case-insensitive list of supported gamepad-buttons is:
/// - north, east, south, west
/// - d-pad-up, d-pad-down, d-pad-left, d-pad-right
/// - left-trigger, left-trigger2
/// - right-trigger, right-trigger2
/// - select, start
///
/// A case-insensitive (non-exhaustive) list of common key-names is:
/// - a, b, c, ..., z
/// - up, down, left, right
/// - return
/// - space
/// - comma, semicolon
/// - key0, key1, ..., key9
/// The complete list of supported key-names can be found by using -B start=nonexistent.
#[clap(
long,
short = 'B',
value_parser(parse_gamepad_button),
verbatim_doc_comment,
value_name = "GAMEPAD BUTTON>=<KEY NAME"
)]
pub gamepad_button: Vec<(GamepadButton, KeyCode)>,
}
fn parse_movie_file_or_url(path: &str) -> Result<Url, Error> {
crate::util::parse_url(Path::new(path))
}
fn parse_gamepad_button(mapping: &str) -> Result<(GamepadButton, KeyCode), Error> {
let pos = mapping.find('=').ok_or_else(|| {
anyhow!("invalid <gamepad button>=<key name>: no `=` found in `{mapping}`")
})?;
fn to_aliases<T: ValueEnum>(variants: &[T]) -> String {
let aliases: Vec<String> = variants
.iter()
.map(|variant| {
variant
.to_possible_value()
.expect("Must have a PossibleValue")
.get_name_and_aliases()
.next()
.expect("Must have one alias")
.to_owned()
})
.collect();
aliases.join(", ")
}
let button = GamepadButton::from_str(&mapping[..pos], true).map_err(|err| {
anyhow!(
"Could not parse <gamepad button>: {err}\n The possible values are: {}",
to_aliases(GamepadButton::value_variants())
)
})?;
let key_code = KeyCode::from_str(&mapping[pos + 1..], true).map_err(|err| {
anyhow!(
"Could not parse <key name>: {err}\n The possible values are: {}",
to_aliases(KeyCode::value_variants())
)
})?;
Ok((button, key_code))
}
impl Opt {
#[cfg(feature = "render_trace")]
pub fn trace_path(&self) -> Option<&Path> {

View File

@ -10,6 +10,7 @@ use crate::{CALLSTACK, RENDER_INFO, SWF_INFO};
use anyhow::anyhow;
use ruffle_core::backend::navigator::{OpenURLMode, SocketMode};
use ruffle_core::config::Letterbox;
use ruffle_core::events::{GamepadButton, KeyCode};
use ruffle_core::{
DefaultFont, LoadBehavior, Player, PlayerBuilder, PlayerEvent, PlayerRuntime, StageAlign,
StageScaleMode,
@ -18,7 +19,7 @@ use ruffle_render::backend::RenderBackend;
use ruffle_render::quality::StageQuality;
use ruffle_render_wgpu::backend::WgpuRenderBackend;
use ruffle_render_wgpu::descriptors::Descriptors;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Duration;
@ -52,6 +53,7 @@ pub struct PlayerOptions {
pub frame_rate: Option<f64>,
pub open_url_mode: OpenURLMode,
pub dummy_external_interface: bool,
pub gamepad_button_mapping: HashMap<GamepadButton, KeyCode>,
}
impl From<&Opt> for PlayerOptions {
@ -79,6 +81,7 @@ impl From<&Opt> for PlayerOptions {
dummy_external_interface: value.dummy_external_interface,
socket_allowed: HashSet::from_iter(value.socket_allow.iter().cloned()),
tcp_connections: value.tcp_connections,
gamepad_button_mapping: HashMap::from_iter(value.gamepad_button.iter().cloned()),
}
}
}
@ -144,6 +147,10 @@ impl ActivePlayer {
Duration::from_secs_f64(opt.max_execution_duration)
};
if !opt.gamepad_button_mapping.is_empty() {
builder = builder.with_gamepad_button_mapping(opt.gamepad_button_mapping.clone());
}
builder = builder
.with_navigator(navigator)
.with_renderer(renderer)

View File

@ -1,7 +1,8 @@
use crate::custom_event::RuffleEvent;
use anyhow::{anyhow, Error};
use gilrs::Button;
use rfd::FileDialog;
use ruffle_core::events::{KeyCode, TextControlCode};
use ruffle_core::events::{GamepadButton, KeyCode, TextControlCode};
use std::path::{Path, PathBuf};
use url::Url;
use winit::dpi::PhysicalSize;
@ -171,6 +172,28 @@ pub fn winit_to_ruffle_key_code(event: &KeyEvent) -> KeyCode {
}
}
pub fn gilrs_button_to_gamepad_button(button: Button) -> Option<GamepadButton> {
match button {
Button::South => Some(GamepadButton::South),
Button::East => Some(GamepadButton::East),
Button::North => Some(GamepadButton::North),
Button::West => Some(GamepadButton::West),
Button::LeftTrigger => Some(GamepadButton::LeftTrigger),
Button::LeftTrigger2 => Some(GamepadButton::LeftTrigger2),
Button::RightTrigger => Some(GamepadButton::RightTrigger),
Button::RightTrigger2 => Some(GamepadButton::RightTrigger2),
Button::Select => Some(GamepadButton::Select),
Button::Start => Some(GamepadButton::Start),
Button::DPadUp => Some(GamepadButton::DPadUp),
Button::DPadDown => Some(GamepadButton::DPadDown),
Button::DPadLeft => Some(GamepadButton::DPadLeft),
Button::DPadRight => Some(GamepadButton::DPadRight),
// GilRs has some more buttons that are seemingly not supported anywhere
// like C or Z.
_ => None,
}
}
pub fn get_screen_size(event_loop: &EventLoop<RuffleEvent>) -> PhysicalSize<u32> {
let mut min_x = 0;
let mut min_y = 0;