1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![forbid(unsafe_code)]
3#![doc(
4 html_logo_url = "https://bevy.org/assets/icon.png",
5 html_favicon_url = "https://bevy.org/assets/icon.png"
6)]
7
8mod converter;
14mod gilrs_system;
15mod rumble;
16
17#[cfg(not(target_arch = "wasm32"))]
18use bevy_platform::cell::SyncCell;
19
20#[cfg(target_arch = "wasm32")]
21use core::cell::RefCell;
22
23use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate};
24use bevy_ecs::entity::EntityHashMap;
25use bevy_ecs::prelude::*;
26use bevy_input::InputSystems;
27use bevy_platform::collections::HashMap;
28use gilrs::GilrsBuilder;
29use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
30use rumble::{play_gilrs_rumble, RunningRumbleEffects};
31use tracing::error;
32
33#[cfg(target_arch = "wasm32")]
34thread_local! {
35 pub static GILRS: RefCell<Option<gilrs::Gilrs>> = const { RefCell::new(None) };
43}
44
45#[derive(Resource)]
46pub(crate) struct Gilrs {
47 #[cfg(not(target_arch = "wasm32"))]
48 cell: SyncCell<gilrs::Gilrs>,
49}
50
51impl Gilrs {
52 #[inline]
53 pub fn with(&mut self, f: impl FnOnce(&mut gilrs::Gilrs)) {
54 #[cfg(target_arch = "wasm32")]
55 GILRS.with(|g| f(g.borrow_mut().as_mut().expect("GILRS was not initialized")));
56 #[cfg(not(target_arch = "wasm32"))]
57 f(self.cell.get());
58 }
59}
60
61#[derive(Debug, Default, Resource)]
63pub(crate) struct GilrsGamepads {
64 pub(crate) entity_to_id: EntityHashMap<gilrs::GamepadId>,
66 pub(crate) id_to_entity: HashMap<gilrs::GamepadId, Entity>,
68}
69
70impl GilrsGamepads {
71 pub fn get_entity(&self, gamepad_id: gilrs::GamepadId) -> Option<Entity> {
73 self.id_to_entity.get(&gamepad_id).copied()
74 }
75
76 pub fn get_gamepad_id(&self, entity: Entity) -> Option<gilrs::GamepadId> {
78 self.entity_to_id.get(&entity).copied()
79 }
80}
81
82#[derive(Default)]
84pub struct GilrsPlugin;
85
86#[derive(Debug, PartialEq, Eq, Clone, Hash, SystemSet)]
88pub struct RumbleSystems;
89
90impl Plugin for GilrsPlugin {
91 fn build(&self, app: &mut App) {
92 match GilrsBuilder::new()
93 .with_default_filters(false)
94 .set_update_state(false)
95 .build()
96 {
97 Ok(gilrs) => {
98 let g = Gilrs {
99 #[cfg(not(target_arch = "wasm32"))]
100 cell: SyncCell::new(gilrs),
101 };
102 #[cfg(target_arch = "wasm32")]
103 GILRS.with(|g| {
104 g.replace(Some(gilrs));
105 });
106 app.insert_resource(g);
107 app.init_resource::<GilrsGamepads>();
108 app.init_resource::<RunningRumbleEffects>()
109 .add_systems(PreStartup, gilrs_event_startup_system)
110 .add_systems(PreUpdate, gilrs_event_system.before(InputSystems))
111 .add_systems(PostUpdate, play_gilrs_rumble.in_set(RumbleSystems));
112 }
113 Err(err) => error!("Failed to start Gilrs. {}", err),
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
124 fn world_is_truly_send() {
125 let mut app = App::new();
126 app.add_plugins(GilrsPlugin);
127 let world = core::mem::take(app.world_mut());
128
129 let handler = std::thread::spawn(move || {
130 drop(world);
131 });
132
133 handler.join().unwrap();
134 }
135}