diff --git a/Cargo.lock b/Cargo.lock index a8aa33de9..fdfa83a38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 246db80d2..747acdc8a 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -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 } diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 9a12082ad..381315ae1 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -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(); diff --git a/desktop/src/cli.rs b/desktop/src/cli.rs index ee4702ade..c36b93841 100644 --- a/desktop/src/cli.rs +++ b/desktop/src/cli.rs @@ -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>=, } fn parse_movie_file_or_url(path: &str) -> Result { 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 =: no `=` found in `{mapping}`") + })?; + + fn to_aliases(variants: &[T]) -> String { + let aliases: Vec = 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 : {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 : {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> { diff --git a/desktop/src/player.rs b/desktop/src/player.rs index 868ab7299..7a3db122d 100644 --- a/desktop/src/player.rs +++ b/desktop/src/player.rs @@ -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, pub open_url_mode: OpenURLMode, pub dummy_external_interface: bool, + pub gamepad_button_mapping: HashMap, } 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) diff --git a/desktop/src/util.rs b/desktop/src/util.rs index 28ae80d82..86e257c44 100644 --- a/desktop/src/util.rs +++ b/desktop/src/util.rs @@ -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 { + 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) -> PhysicalSize { let mut min_x = 0; let mut min_y = 0;