Skip to content

Commit 2139373

Browse files
committed
web: Support gamepad button input
1 parent 46aa652 commit 2139373

File tree

9 files changed

+156
-6
lines changed

9 files changed

+156
-6
lines changed

core/src/events.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::display_object::InteractiveObject;
2+
use std::str::FromStr;
23
use swf::ClipEventFlag;
34

45
#[derive(Debug, Clone, Copy)]
@@ -801,3 +802,29 @@ pub enum GamepadButton {
801802
DPadLeft,
802803
DPadRight,
803804
}
805+
806+
pub struct ParseEnumError;
807+
808+
impl FromStr for GamepadButton {
809+
type Err = ParseEnumError;
810+
811+
fn from_str(s: &str) -> Result<Self, Self::Err> {
812+
Ok(match s {
813+
"south" => Self::South,
814+
"east" => Self::East,
815+
"north" => Self::North,
816+
"west" => Self::West,
817+
"left-trigger" => Self::LeftTrigger,
818+
"left-trigger-2" => Self::LeftTrigger2,
819+
"right-trigger" => Self::RightTrigger,
820+
"right-trigger-2" => Self::RightTrigger2,
821+
"select" => Self::Select,
822+
"start" => Self::Start,
823+
"dpad-up" => Self::DPadUp,
824+
"dpad-down" => Self::DPadDown,
825+
"dpad-left" => Self::DPadLeft,
826+
"dpad-right" => Self::DPadRight,
827+
_ => return Err(ParseEnumError),
828+
})
829+
}
830+
}

desktop/src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ fn parse_gamepad_button(mapping: &str) -> Result<(GamepadButton, KeyCode), Error
280280
aliases.join(", ")
281281
}
282282

283-
let button = GamepadButton::from_str(&mapping[..pos], true).map_err(|err| {
283+
let button = <GamepadButton as ValueEnum>::from_str(&mapping[..pos], true).map_err(|err| {
284284
anyhow!(
285285
"Could not parse <gamepad button>: {err}\n The possible values are: {}",
286286
to_aliases(GamepadButton::value_variants())

web/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ features = [
7474
"EventTarget", "GainNode", "Headers", "HtmlCanvasElement", "HtmlDocument", "HtmlElement", "HtmlFormElement",
7575
"HtmlInputElement", "HtmlTextAreaElement", "KeyboardEvent", "Location", "PointerEvent",
7676
"Request", "RequestInit", "Response", "Storage", "WheelEvent", "Window", "ReadableStream", "RequestCredentials",
77-
"Url", "Clipboard", "FocusEvent", "ShadowRoot"
77+
"Url", "Clipboard", "FocusEvent", "ShadowRoot", "Gamepad", "GamepadButton"
7878
]
7979

8080
[package.metadata.cargo-machete]

web/eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export default tseslint.config(
117117
],
118118
"jsdoc/check-tag-names": [
119119
"error",
120-
{ definedTags: ["privateRemarks", "remarks"] },
120+
{ definedTags: ["experimental", "privateRemarks", "remarks"] },
121121
],
122122
},
123123
settings: {

web/packages/core/src/internal/builder.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ export function configureBuilder(
109109
builder.addSocketProxy(proxy.host, proxy.port, proxy.proxyUrl);
110110
}
111111
}
112+
113+
if (isExplicit(config.gamepadButtonMapping)) {
114+
for (const [button, keyCode] of Object.entries(
115+
config.gamepadButtonMapping,
116+
)) {
117+
builder.addGamepadButtonMapping(button, keyCode);
118+
}
119+
}
112120
}
113121

114122
/**

web/packages/core/src/public/config/default.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ export const DEFAULT_CONFIG: Required<BaseLoadOptions> = {
5151
defaultFonts: {},
5252
credentialAllowList: [],
5353
playerRuntime: PlayerRuntime.FlashPlayer,
54+
gamepadButtonMapping: {},
5455
};

web/packages/core/src/public/config/load-options.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,26 @@ export interface DefaultFonts {
321321
japaneseMincho?: Array<string>;
322322
}
323323

324+
/**
325+
* @experimental
326+
*/
327+
export enum GamepadButton {
328+
South = "south",
329+
East = "east",
330+
North = "north",
331+
West = "west",
332+
LeftTrigger = "left-trigger",
333+
LeftTrigger2 = "left-trigger-2",
334+
RightTrigger = "right-trigger",
335+
RightTrigger2 = "right-trigger-2",
336+
Select = "select",
337+
Start = "start",
338+
DPadUp = "dpad-up",
339+
DPadDown = "dpad-down",
340+
DPadLeft = "dpad-left",
341+
DPadRight = "dpad-right",
342+
}
343+
324344
/**
325345
* Any options used for loading a movie.
326346
*/
@@ -667,6 +687,27 @@ export interface BaseLoadOptions {
667687
* This allows you to emulate Adobe AIR or Adobe Flash Player.
668688
*/
669689
playerRuntime?: PlayerRuntime;
690+
691+
/**
692+
* An object mapping gamepad button names to ActionScript key codes.
693+
*
694+
* With the appropriate mapping pressing a button on the gamepad will look like the corresponding key press to the loaded SWF.
695+
* This can be used for adding gamepad support to games that don't support it otherwise.
696+
*
697+
* An example config for mapping the D-pad to the arrow keys would look like this:
698+
* `
699+
* {
700+
* "dpad-up": 38,
701+
* "dpad-down": 40,
702+
* "dpad-left": 37,
703+
* "dpad-right": 39,
704+
* }
705+
* `
706+
*
707+
* @experimental
708+
* @default {}
709+
*/
710+
gamepadButtonMapping?: Partial<Record<GamepadButton, number>>;
670711
}
671712

672713
/**

web/src/builder.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use ruffle_core::backend::storage::{MemoryStorageBackend, StorageBackend};
1010
use ruffle_core::backend::ui::FontDefinition;
1111
use ruffle_core::compatibility_rules::CompatibilityRules;
1212
use ruffle_core::config::{Letterbox, NetworkingAccessMode};
13+
use ruffle_core::events::{GamepadButton, KeyCode};
1314
use ruffle_core::ttf_parser;
1415
use ruffle_core::{
1516
swf, Color, DefaultFont, Player, PlayerBuilder, PlayerRuntime, StageAlign, StageScaleMode,
@@ -61,6 +62,7 @@ pub struct RuffleInstanceBuilder {
6162
pub(crate) volume: f32,
6263
pub(crate) default_fonts: HashMap<DefaultFont, Vec<String>>,
6364
pub(crate) custom_fonts: Vec<(String, Vec<u8>)>,
65+
pub(crate) gamepad_button_mapping: HashMap<GamepadButton, KeyCode>,
6466
}
6567

6668
impl Default for RuffleInstanceBuilder {
@@ -97,6 +99,7 @@ impl Default for RuffleInstanceBuilder {
9799
volume: 1.0,
98100
default_fonts: HashMap::new(),
99101
custom_fonts: vec![],
102+
gamepad_button_mapping: HashMap::new(),
100103
}
101104
}
102105
}
@@ -317,6 +320,14 @@ impl RuffleInstanceBuilder {
317320
);
318321
}
319322

323+
#[wasm_bindgen(js_name = "addGamepadButtonMapping")]
324+
pub fn add_gampepad_button_mapping(&mut self, button: &str, keycode: u32) {
325+
if let Ok(button) = GamepadButton::from_str(button) {
326+
self.gamepad_button_mapping
327+
.insert(button, KeyCode::from_code(keycode));
328+
}
329+
}
330+
320331
// TODO: This should be split into two methods that either load url or load data
321332
// Right now, that's done immediately afterwards in TS
322333
pub async fn build(&self, parent: HtmlElement, js_player: JavascriptPlayer) -> Promise {
@@ -657,6 +668,7 @@ impl RuffleInstanceBuilder {
657668
.with_scale_mode(self.scale, self.force_scale)
658669
.with_frame_rate(self.frame_rate)
659670
.with_page_url(window.location().href().ok())
671+
.with_gamepad_button_mapping(self.gamepad_button_mapping.clone())
660672
.build();
661673

662674
let player_weak = Arc::downgrade(&core);

web/src/lib.rs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use input::{web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_con
1818
use js_sys::{Error as JsError, Uint8Array};
1919
use ruffle_core::context::UpdateContext;
2020
use ruffle_core::context_menu::ContextMenuCallback;
21-
use ruffle_core::events::{MouseButton, MouseWheelDelta, TextControlCode};
21+
use ruffle_core::events::{GamepadButton, MouseButton, MouseWheelDelta, TextControlCode};
2222
use ruffle_core::tag_utils::SwfMovie;
2323
use ruffle_core::{Player, PlayerEvent, StaticCallstack, ViewportDimensions};
2424
use ruffle_web_common::JsResult;
@@ -38,8 +38,8 @@ use wasm_bindgen::convert::FromWasmAbi;
3838
use wasm_bindgen::prelude::*;
3939
use web_sys::{
4040
AddEventListenerOptions, ClipboardEvent, Element, Event, EventTarget, FocusEvent,
41-
HtmlCanvasElement, HtmlElement, KeyboardEvent, Node, PointerEvent, ShadowRoot, WheelEvent,
42-
Window,
41+
Gamepad as WebGamepad, GamepadButton as WebGamepadButton, HtmlCanvasElement, HtmlElement,
42+
KeyboardEvent, Node, PointerEvent, ShadowRoot, WheelEvent, Window,
4343
};
4444

4545
static RUFFLE_GLOBAL_PANIC: Once = Once::new();
@@ -140,6 +140,7 @@ struct RuffleInstance {
140140
has_focus: bool,
141141
trace_observer: Rc<RefCell<JsValue>>,
142142
log_subscriber: Arc<Layered<WASMLayer, Registry>>,
143+
pressed_buttons: Vec<GamepadButton>,
143144
}
144145

145146
#[wasm_bindgen(raw_module = "./internal/player/inner")]
@@ -508,6 +509,7 @@ impl RuffleHandle {
508509
has_focus: false,
509510
trace_observer: player.trace_observer,
510511
log_subscriber,
512+
pressed_buttons: vec![],
511513
};
512514

513515
// Prevent touch-scrolling on canvas.
@@ -1015,6 +1017,7 @@ impl RuffleHandle {
10151017
fn tick(&mut self, timestamp: f64) {
10161018
let mut dt = 0.0;
10171019
let mut new_dimensions = None;
1020+
let mut gamepad_button_events = Vec::new();
10181021
let _ = self.with_instance_mut(|instance| {
10191022
// Check for canvas resize.
10201023
let canvas_width = instance.canvas.client_width();
@@ -1043,6 +1046,60 @@ impl RuffleHandle {
10431046
));
10441047
}
10451048

1049+
if let Ok(gamepads) = instance.window.navigator().get_gamepads() {
1050+
if let Some(gamepad) = gamepads
1051+
.into_iter()
1052+
.next()
1053+
.and_then(|gamepad| gamepad.dyn_into::<WebGamepad>().ok())
1054+
{
1055+
let mut pressed_buttons = Vec::new();
1056+
1057+
let buttons = gamepad.buttons();
1058+
for (index, button) in buttons.into_iter().enumerate() {
1059+
let Ok(button) = button.dyn_into::<WebGamepadButton>() else {
1060+
continue;
1061+
};
1062+
1063+
if !button.pressed() {
1064+
continue;
1065+
}
1066+
1067+
// See https://w3c.github.io/gamepad/#remapping
1068+
let gamepad_button = match index {
1069+
0 => GamepadButton::South,
1070+
1 => GamepadButton::East,
1071+
2 => GamepadButton::West,
1072+
3 => GamepadButton::North,
1073+
12 => GamepadButton::DPadUp,
1074+
13 => GamepadButton::DPadDown,
1075+
14 => GamepadButton::DPadLeft,
1076+
15 => GamepadButton::DPadRight,
1077+
_ => continue,
1078+
};
1079+
1080+
pressed_buttons.push(gamepad_button);
1081+
}
1082+
1083+
if pressed_buttons != instance.pressed_buttons {
1084+
for button in pressed_buttons.iter() {
1085+
if !instance.pressed_buttons.contains(button) {
1086+
gamepad_button_events
1087+
.push(PlayerEvent::GamepadButtonDown { button: *button });
1088+
}
1089+
}
1090+
1091+
for button in instance.pressed_buttons.iter() {
1092+
if !pressed_buttons.contains(button) {
1093+
gamepad_button_events
1094+
.push(PlayerEvent::GamepadButtonUp { button: *button });
1095+
}
1096+
}
1097+
1098+
instance.pressed_buttons = pressed_buttons;
1099+
}
1100+
}
1101+
}
1102+
10461103
// Request next animation frame.
10471104
if let Some(handler) = &instance.animation_handler {
10481105
let id = instance
@@ -1065,6 +1122,10 @@ impl RuffleHandle {
10651122

10661123
// Tick the Ruffle core.
10671124
let _ = self.with_core_mut(|core| {
1125+
for event in gamepad_button_events {
1126+
core.handle_event(event);
1127+
}
1128+
10681129
if let Some((ref canvas, viewport_width, viewport_height, device_pixel_ratio)) =
10691130
new_dimensions
10701131
{

0 commit comments

Comments
 (0)