Skip to content

Commit

Permalink
web: Support gamepad button input
Browse files Browse the repository at this point in the history
  • Loading branch information
evilpie committed Jan 5, 2025
1 parent f101687 commit fc28dd4
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 6 deletions.
27 changes: 27 additions & 0 deletions core/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::display_object::InteractiveObject;
use std::str::FromStr;
use swf::ClipEventFlag;

#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -801,3 +802,29 @@ pub enum GamepadButton {
DPadLeft,
DPadRight,
}

pub struct ParseEnumError;

impl FromStr for GamepadButton {
type Err = ParseEnumError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"south" => Self::South,
"east" => Self::East,
"north" => Self::North,
"west" => Self::West,
"left-trigger" => Self::LeftTrigger,
"left-trigger-2" => Self::LeftTrigger2,
"right-trigger" => Self::RightTrigger,
"right-trigger-2" => Self::RightTrigger2,
"select" => Self::Select,
"start" => Self::Start,
"dpad-up" => Self::DPadUp,
"dpad-down" => Self::DPadDown,
"dpad-left" => Self::DPadLeft,
"dpad-right" => Self::DPadRight,
_ => return Err(ParseEnumError),
})
}
}
2 changes: 1 addition & 1 deletion desktop/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ fn parse_gamepad_button(mapping: &str) -> Result<(GamepadButton, KeyCode), Error
aliases.join(", ")
}

let button = GamepadButton::from_str(&mapping[..pos], true).map_err(|err| {
let button = <GamepadButton as ValueEnum>::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())
Expand Down
2 changes: 1 addition & 1 deletion web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ features = [
"EventTarget", "GainNode", "Headers", "HtmlCanvasElement", "HtmlDocument", "HtmlElement", "HtmlFormElement",
"HtmlInputElement", "HtmlTextAreaElement", "KeyboardEvent", "Location", "PointerEvent",
"Request", "RequestInit", "Response", "Storage", "WheelEvent", "Window", "ReadableStream", "RequestCredentials",
"Url", "Clipboard", "FocusEvent", "ShadowRoot"
"Url", "Clipboard", "FocusEvent", "ShadowRoot", "Gamepad", "GamepadButton"
]

[package.metadata.cargo-machete]
Expand Down
2 changes: 1 addition & 1 deletion web/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export default tseslint.config(
],
"jsdoc/check-tag-names": [
"error",
{ definedTags: ["privateRemarks", "remarks"] },
{ definedTags: ["experimental", "privateRemarks", "remarks"] },
],
},
settings: {
Expand Down
8 changes: 8 additions & 0 deletions web/packages/core/src/internal/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ export function configureBuilder(
builder.addSocketProxy(proxy.host, proxy.port, proxy.proxyUrl);
}
}

if (isExplicit(config.gamepadButtonMapping)) {
for (const [button, keyCode] of Object.entries(
config.gamepadButtonMapping,
)) {
builder.addGamepadButtonMapping(button, keyCode);
}
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions web/packages/core/src/public/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ export const DEFAULT_CONFIG: Required<BaseLoadOptions> = {
defaultFonts: {},
credentialAllowList: [],
playerRuntime: PlayerRuntime.FlashPlayer,
gamepadButtonMapping: {},
};
41 changes: 41 additions & 0 deletions web/packages/core/src/public/config/load-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,26 @@ export interface DefaultFonts {
japaneseMincho?: Array<string>;
}

/**
* @experimental
*/
export enum GamepadButton {
South = "south",
East = "east",
North = "north",
West = "west",
LeftTrigger = "left-trigger",
LeftTrigger2 = "left-trigger-2",
RightTrigger = "right-trigger",
RightTrigger2 = "right-trigger-2",
Select = "select",
Start = "start",
DPadUp = "dpad-up",
DPadDown = "dpad-down",
DPadLeft = "dpad-left",
DPadRight = "dpad-right",
}

/**
* Any options used for loading a movie.
*/
Expand Down Expand Up @@ -667,6 +687,27 @@ export interface BaseLoadOptions {
* This allows you to emulate Adobe AIR or Adobe Flash Player.
*/
playerRuntime?: PlayerRuntime;

/**
* An object mapping gamepad button names to ActionScript key codes.
*
* With the appropriate mapping pressing a button on the gamepad will look like the corresponding key press to the loaded SWF.
* This can be used for adding gamepad support to games that don't support it otherwise.
*
* An example config for mapping the D-pad to the arrow keys would look like this:
* `
* {
* "dpad-up": 38,
* "dpad-down": 40,
* "dpad-left": 37,
* "dpad-right": 39,
* }
* `
*
* @experimental
* @default {}
*/
gamepadButtonMapping?: Partial<Record<GamepadButton, number>>;
}

/**
Expand Down
12 changes: 12 additions & 0 deletions web/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use ruffle_core::backend::storage::{MemoryStorageBackend, StorageBackend};
use ruffle_core::backend::ui::FontDefinition;
use ruffle_core::compatibility_rules::CompatibilityRules;
use ruffle_core::config::{Letterbox, NetworkingAccessMode};
use ruffle_core::events::{GamepadButton, KeyCode};
use ruffle_core::ttf_parser;
use ruffle_core::{
swf, Color, DefaultFont, Player, PlayerBuilder, PlayerRuntime, StageAlign, StageScaleMode,
Expand Down Expand Up @@ -61,6 +62,7 @@ pub struct RuffleInstanceBuilder {
pub(crate) volume: f32,
pub(crate) default_fonts: HashMap<DefaultFont, Vec<String>>,
pub(crate) custom_fonts: Vec<(String, Vec<u8>)>,
pub(crate) gamepad_button_mapping: HashMap<GamepadButton, KeyCode>,
}

impl Default for RuffleInstanceBuilder {
Expand Down Expand Up @@ -97,6 +99,7 @@ impl Default for RuffleInstanceBuilder {
volume: 1.0,
default_fonts: HashMap::new(),
custom_fonts: vec![],
gamepad_button_mapping: HashMap::new(),
}
}
}
Expand Down Expand Up @@ -317,6 +320,14 @@ impl RuffleInstanceBuilder {
);
}

#[wasm_bindgen(js_name = "addGamepadButtonMapping")]
pub fn add_gampepad_button_mapping(&mut self, button: &str, keycode: u32) {
if let Ok(button) = GamepadButton::from_str(button) {
self.gamepad_button_mapping
.insert(button, KeyCode::from_code(keycode));
}
}

// TODO: This should be split into two methods that either load url or load data
// Right now, that's done immediately afterwards in TS
pub async fn build(&self, parent: HtmlElement, js_player: JavascriptPlayer) -> Promise {
Expand Down Expand Up @@ -657,6 +668,7 @@ impl RuffleInstanceBuilder {
.with_scale_mode(self.scale, self.force_scale)
.with_frame_rate(self.frame_rate)
.with_page_url(window.location().href().ok())
.with_gamepad_button_mapping(self.gamepad_button_mapping.clone())
.build();

let player_weak = Arc::downgrade(&core);
Expand Down
62 changes: 59 additions & 3 deletions web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use input::{web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_con
use js_sys::{Error as JsError, Uint8Array};
use ruffle_core::context::UpdateContext;
use ruffle_core::context_menu::ContextMenuCallback;
use ruffle_core::events::{MouseButton, MouseWheelDelta, TextControlCode};
use ruffle_core::events::{GamepadButton, MouseButton, MouseWheelDelta, TextControlCode};
use ruffle_core::tag_utils::SwfMovie;
use ruffle_core::{Player, PlayerEvent, StaticCallstack, ViewportDimensions};
use ruffle_web_common::JsResult;
Expand All @@ -38,8 +38,8 @@ use wasm_bindgen::convert::FromWasmAbi;
use wasm_bindgen::prelude::*;
use web_sys::{
AddEventListenerOptions, ClipboardEvent, Element, Event, EventTarget, FocusEvent,
HtmlCanvasElement, HtmlElement, KeyboardEvent, Node, PointerEvent, ShadowRoot, WheelEvent,
Window,
Gamepad as WebGamepad, GamepadButton as WebGamepadButton, HtmlCanvasElement, HtmlElement,
KeyboardEvent, Node, PointerEvent, ShadowRoot, WheelEvent, Window,
};

static RUFFLE_GLOBAL_PANIC: Once = Once::new();
Expand Down Expand Up @@ -140,6 +140,7 @@ struct RuffleInstance {
has_focus: bool,
trace_observer: Rc<RefCell<JsValue>>,
log_subscriber: Arc<Layered<WASMLayer, Registry>>,
pressed_buttons: Vec<GamepadButton>,
}

#[wasm_bindgen(raw_module = "./internal/player/inner")]
Expand Down Expand Up @@ -508,6 +509,7 @@ impl RuffleHandle {
has_focus: false,
trace_observer: player.trace_observer,
log_subscriber,
pressed_buttons: vec![],
};

// Prevent touch-scrolling on canvas.
Expand Down Expand Up @@ -1015,6 +1017,7 @@ impl RuffleHandle {
fn tick(&mut self, timestamp: f64) {
let mut dt = 0.0;
let mut new_dimensions = None;
let mut gamepad_button_events = Vec::new();
let _ = self.with_instance_mut(|instance| {
// Check for canvas resize.
let canvas_width = instance.canvas.client_width();
Expand Down Expand Up @@ -1043,6 +1046,55 @@ impl RuffleHandle {
));
}

if let Ok(gamepads) = instance.window.navigator().get_gamepads() {
if let Some(gamepad) = gamepads.into_iter().next() {
let gamepad: WebGamepad = gamepad.dyn_into().unwrap();

let mut pressed_buttons = Vec::new();

let buttons = gamepad.buttons();
for (index, button) in buttons.into_iter().enumerate() {
let button: WebGamepadButton = button.dyn_into().unwrap();
if !button.pressed() {
continue;
}

// See https://w3c.github.io/gamepad/#remapping
let gamepad_button = match index {
0 => GamepadButton::South,
1 => GamepadButton::East,
2 => GamepadButton::West,
3 => GamepadButton::North,
12 => GamepadButton::DPadUp,
13 => GamepadButton::DPadDown,
14 => GamepadButton::DPadLeft,
15 => GamepadButton::DPadRight,
_ => continue,
};

pressed_buttons.push(gamepad_button);
}

if pressed_buttons != instance.pressed_buttons {
for button in pressed_buttons.iter() {
if !instance.pressed_buttons.contains(button) {
gamepad_button_events
.push(PlayerEvent::GamepadButtonDown { button: *button });
}
}

for button in instance.pressed_buttons.iter() {
if !pressed_buttons.contains(&button) {
gamepad_button_events
.push(PlayerEvent::GamepadButtonUp { button: *button });
}
}

instance.pressed_buttons = pressed_buttons;
}
}
}

// Request next animation frame.
if let Some(handler) = &instance.animation_handler {
let id = instance
Expand All @@ -1065,6 +1117,10 @@ impl RuffleHandle {

// Tick the Ruffle core.
let _ = self.with_core_mut(|core| {
for event in gamepad_button_events {
core.handle_event(event);
}

if let Some((ref canvas, viewport_width, viewport_height, device_pixel_ratio)) =
new_dimensions
{
Expand Down

0 comments on commit fc28dd4

Please sign in to comment.