devcaders/
lib.rs

1//! Library for Rusty Devcade games using bevy!
2//!
3//! # Input Handling
4//! See [The example for `DevcadeControls`](DevcadeControls#examples)
5use async_compat::Compat;
6use bevy::ecs::component::Tick;
7use bevy::ecs::system::{SystemMeta, SystemParam};
8use bevy::ecs::world::unsafe_world_cell::UnsafeWorldCell;
9use bevy::prelude::*;
10use bevy::tasks::{AsyncComputeTaskPool, Task};
11pub use devcade_onboard_types;
12use devcade_onboard_types::{Map, Player as BackendPlayer, RequestBody, ResponseBody, Value};
13use enum_iterator::Sequence;
14use futures_lite::future;
15use std::ops::Deref;
16use std::sync::OnceLock;
17
18#[cfg(not(target_os = "windows"))]
19mod client;
20#[cfg(not(target_os = "windows"))]
21pub use client::{BackendClient, RequestError};
22
23#[derive(SystemParam)]
24struct DevcadeControlsInner<'w> {
25  gamepads: Res<'w, Gamepads>,
26  button_inputs: Res<'w, Input<GamepadButton>>,
27  axes: Res<'w, Axis<GamepadAxis>>,
28  keyboard_input: Res<'w, Input<KeyCode>>,
29}
30
31/// [`SystemParam`] for devcade's control buttons
32///
33/// # Examples
34/// Usage is simple, just add it as a parameter to one of your [`System`](bevy::ecs::system::System)s!
35/// ```
36/// use devcaders::{Button, Player, DevcadeControls};
37///
38/// fn input_system(button_inputs: DevcadeControls) {
39///   // User is actively pressing Menu button
40///   if button_inputs.pressed(Player::P1, Button::Menu) {
41///     std::process::exit(0);
42///   }
43///   let mut x_vector = 0;
44///   // User pressed StickRight button
45///   if button_inputs.just_pressed(Player::P1, Button::StickRight) {
46///     x_vector += 1;
47///   }
48///   // User released StickLeft button
49///   if button_inputs.just_released(Player::P1, Button::StickLeft) {
50///     x_vector -= 1;
51///   }
52/// }
53/// ```
54pub struct DevcadeControls {
55  p1: PlayerControlState,
56  p2: PlayerControlState,
57}
58#[derive(Default, Clone)]
59struct ButtonState {
60  pressed: bool,
61  changed_this_frame: bool,
62}
63#[derive(Default, Clone)]
64struct PlayerControlState {
65  stick_up: ButtonState,
66  stick_down: ButtonState,
67  stick_left: ButtonState,
68  stick_right: ButtonState,
69  menu: ButtonState,
70  a1: ButtonState,
71  a2: ButtonState,
72  a3: ButtonState,
73  a4: ButtonState,
74  b1: ButtonState,
75  b2: ButtonState,
76  b3: ButtonState,
77  b4: ButtonState,
78}
79
80impl PlayerControlState {
81  fn get_state_for(&self, button: Button) -> &ButtonState {
82    match button {
83      Button::StickUp => &self.stick_up,
84      Button::StickDown => &self.stick_down,
85      Button::StickLeft => &self.stick_left,
86      Button::StickRight => &self.stick_right,
87      Button::A1 => &self.a1,
88      Button::A2 => &self.a2,
89      Button::A3 => &self.a3,
90      Button::A4 => &self.a4,
91      Button::B1 => &self.b1,
92      Button::B2 => &self.b2,
93      Button::B3 => &self.b3,
94      Button::B4 => &self.b4,
95      Button::Menu => &self.menu,
96    }
97  }
98
99  fn get_state_for_mut(&mut self, button: Button) -> &mut ButtonState {
100    match button {
101      Button::StickUp => &mut self.stick_up,
102      Button::StickDown => &mut self.stick_down,
103      Button::StickLeft => &mut self.stick_left,
104      Button::StickRight => &mut self.stick_right,
105      Button::A1 => &mut self.a1,
106      Button::A2 => &mut self.a2,
107      Button::A3 => &mut self.a3,
108      Button::A4 => &mut self.a4,
109      Button::B1 => &mut self.b1,
110      Button::B2 => &mut self.b2,
111      Button::B3 => &mut self.b3,
112      Button::B4 => &mut self.b4,
113      Button::Menu => &mut self.menu,
114    }
115  }
116}
117
118/// Underlying state of [`DevcadeControls`]
119pub struct ControlState<'w> {
120  p1: PlayerControlState,
121  p2: PlayerControlState,
122  inner: <DevcadeControlsInner<'w> as SystemParam>::State,
123}
124
125unsafe impl SystemParam for DevcadeControls {
126  type State = ControlState<'static>;
127  type Item<'w, 's> = DevcadeControls;
128  fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State {
129    Self::State {
130      inner: DevcadeControlsInner::init_state(world, system_meta),
131      p1: PlayerControlState::default(),
132      p2: PlayerControlState::default(),
133    }
134  }
135  unsafe fn get_param<'w, 's>(
136    state: &'s mut Self::State,
137    system_meta: &SystemMeta,
138    world: UnsafeWorldCell<'w>,
139    change_tick: Tick,
140  ) -> Self::Item<'w, 's> {
141    let inner = DevcadeControlsInner::get_param(&mut state.inner, system_meta, world, change_tick);
142    for player in enum_iterator::all::<Player>() {
143      let player_state = match player {
144        Player::P1 => &mut state.p1,
145        Player::P2 => &mut state.p2,
146      };
147      for button in enum_iterator::all::<Button>() {
148        let button_state = player_state.get_state_for_mut(button);
149        let pressed = inner.pressed(button, player);
150        button_state.changed_this_frame = pressed != button_state.pressed;
151        button_state.pressed = pressed;
152      }
153    }
154    DevcadeControls {
155      p1: state.p1.clone(),
156      p2: state.p2.clone(),
157    }
158  }
159}
160
161impl DevcadeControls {
162  fn get_player(&self, player: Player) -> &PlayerControlState {
163    match player {
164      Player::P1 => &self.p1,
165      Player::P2 => &self.p2,
166    }
167  }
168
169  /// Returns true when button began being pressed on this frame, false otherwise
170  pub fn just_pressed(&self, player: Player, button: Button) -> bool {
171    let player = self.get_player(player);
172    let button_state = player.get_state_for(button);
173    button_state.pressed && button_state.changed_this_frame
174  }
175  /// Returns true when button began being unpressed on this frame, false otherwise
176  pub fn just_released(&self, player: Player, button: Button) -> bool {
177    let player = self.get_player(player);
178    let button_state = player.get_state_for(button);
179    !button_state.pressed && button_state.changed_this_frame
180  }
181  /// Returns true if the button is currently pressed
182  pub fn pressed(&self, player: Player, button: Button) -> bool {
183    self.get_player(player).get_state_for(button).pressed
184  }
185}
186
187#[derive(Debug, Clone, Copy, Sequence, PartialEq, Eq)]
188/// Gamepad buttons
189pub enum Button {
190  /// Top row, first button. Red
191  A1,
192  /// Top row, second button. Blue
193  A2,
194  /// Top row, third button. Green
195  A3,
196  /// Top row, fourth button. White
197  A4,
198
199  /// Second row, first button.
200  B1,
201  /// Second row, second button.
202  B2,
203  /// Second row, third button.
204  B3,
205  /// Second row, third button.
206  B4,
207
208  /// Center button. Black. Generally bound to pause or exit
209  Menu,
210
211  /// Joystick pointing left
212  StickLeft,
213  /// Joystick pointing up
214  StickUp,
215  /// Joystick pointing down
216  StickDown,
217  /// Joystick pointing right
218  StickRight,
219}
220
221impl TryFrom<&Button> for GamepadButtonType {
222  type Error = ();
223  fn try_from(value: &Button) -> Result<Self, Self::Error> {
224    match value {
225      Button::Menu => Ok(GamepadButtonType::Start),
226      Button::A1 => Ok(GamepadButtonType::West),
227      Button::A2 => Ok(GamepadButtonType::North),
228      Button::A3 => Ok(GamepadButtonType::RightTrigger),
229      Button::A4 => Ok(GamepadButtonType::LeftTrigger),
230      Button::B1 => Ok(GamepadButtonType::South),
231      Button::B2 => Ok(GamepadButtonType::East),
232      Button::B3 => Ok(GamepadButtonType::RightTrigger2),
233      Button::B4 => Ok(GamepadButtonType::LeftTrigger2),
234      _ => Err(()),
235    }
236  }
237}
238
239enum AxisConfig {
240  Positive(GamepadAxisType),
241  Negative(GamepadAxisType),
242}
243
244impl AxisConfig {
245  fn get_axis(&self) -> GamepadAxisType {
246    *match self {
247      AxisConfig::Positive(axis_type) => axis_type,
248      AxisConfig::Negative(axis_type) => axis_type,
249    }
250  }
251}
252
253impl TryFrom<&Button> for AxisConfig {
254  type Error = ();
255  fn try_from(value: &Button) -> Result<Self, Self::Error> {
256    match value {
257      Button::StickUp => Ok(AxisConfig::Positive(GamepadAxisType::LeftStickY)),
258      Button::StickDown => Ok(AxisConfig::Negative(GamepadAxisType::LeftStickY)),
259      Button::StickRight => Ok(AxisConfig::Positive(GamepadAxisType::LeftStickX)),
260      Button::StickLeft => Ok(AxisConfig::Negative(GamepadAxisType::LeftStickX)),
261      _ => Err(()),
262    }
263  }
264}
265
266/// Internal. Tuple of [`Player`] and [`Button`]
267pub struct PlayerButton {
268  player: Player,
269  button: Button,
270}
271
272impl From<PlayerButton> for KeyCode {
273  fn from(value: PlayerButton) -> KeyCode {
274    match (value.player, value.button) {
275      (Player::P1, Button::A1) => KeyCode::Q,
276      (Player::P1, Button::A2) => KeyCode::W,
277      (Player::P1, Button::A3) => KeyCode::E,
278      (Player::P1, Button::A4) => KeyCode::R,
279      (Player::P1, Button::B1) => KeyCode::A,
280      (Player::P1, Button::B2) => KeyCode::S,
281      (Player::P1, Button::B3) => KeyCode::D,
282      (Player::P1, Button::B4) => KeyCode::F,
283      (Player::P1, Button::Menu) => KeyCode::Escape,
284      (Player::P1, Button::StickUp) => KeyCode::G,
285      (Player::P1, Button::StickDown) => KeyCode::B,
286      (Player::P1, Button::StickLeft) => KeyCode::V,
287      (Player::P1, Button::StickRight) => KeyCode::N,
288
289      (Player::P2, Button::A1) => KeyCode::Y,
290      (Player::P2, Button::A2) => KeyCode::U,
291      (Player::P2, Button::A3) => KeyCode::I,
292      (Player::P2, Button::A4) => KeyCode::O,
293      (Player::P2, Button::B1) => KeyCode::H,
294      (Player::P2, Button::B2) => KeyCode::J,
295      (Player::P2, Button::B3) => KeyCode::K,
296      (Player::P2, Button::B4) => KeyCode::L,
297      (Player::P2, Button::Menu) => KeyCode::Escape,
298      (Player::P2, Button::StickUp) => KeyCode::Up,
299      (Player::P2, Button::StickDown) => KeyCode::Down,
300      (Player::P2, Button::StickLeft) => KeyCode::Left,
301      (Player::P2, Button::StickRight) => KeyCode::Right,
302    }
303  }
304}
305
306#[derive(Debug, Clone, Copy, Sequence, PartialEq, Eq, Component)]
307/// Used to specify which player's controls to query
308pub enum Player {
309  /// First player, left set of controls
310  P1,
311  /// Second player, right set of controls
312  P2,
313}
314
315impl Player {
316  fn index(&self) -> usize {
317    match self {
318      Self::P1 => 0,
319      Self::P2 => 1,
320    }
321  }
322}
323
324impl<'w> DevcadeControlsInner<'w> {
325  fn gamepad_for_player(&self, player: &Player) -> Option<Gamepad> {
326    self.gamepads.iter().nth(player.index())
327  }
328  /// Returns true if the button is pressed by the given player
329  /// Uses keyboard if no controller is plugged in.
330  /// See source for [`PlayerButton`] for more detailed mappings
331  pub fn pressed(&self, button: Button, player: Player) -> bool {
332    if let Some(gamepad) = self.gamepad_for_player(&player) {
333      if let Ok(button) = GamepadButtonType::try_from(&button) {
334        self
335          .button_inputs
336          .pressed(GamepadButton::new(gamepad, button))
337      } else {
338        let axis_config = AxisConfig::try_from(&button).unwrap();
339        let value = self
340          .axes
341          .get(GamepadAxis::new(gamepad, axis_config.get_axis()))
342          .unwrap();
343        match axis_config {
344          AxisConfig::Positive(_) => value > 0.0,
345          AxisConfig::Negative(_) => value < 0.0,
346        }
347      }
348    } else {
349      self
350        .keyboard_input
351        .pressed(KeyCode::from(PlayerButton { button, player }))
352    }
353  }
354}
355
356/// Close the focused window when both menu buttons are pressed.
357pub fn close_on_menu_buttons(
358  mut commands: Commands,
359  focused_windows: Query<(Entity, &Window)>,
360  input: DevcadeControls,
361) {
362  for (window, focus) in focused_windows.iter() {
363    if !focus.focused {
364      continue;
365    }
366    if input.pressed(Player::P1, Button::Menu) && input.pressed(Player::P2, Button::Menu) {
367      commands.entity(window).despawn();
368    }
369  }
370}
371
372struct CellWrapper<T>(OnceLock<T>);
373impl<T> CellWrapper<T> {
374  const fn new() -> Self {
375    Self(OnceLock::new())
376  }
377}
378
379#[cfg(not(target_os = "windows"))]
380impl Deref for CellWrapper<BackendClient> {
381  type Target = BackendClient;
382  fn deref(&self) -> &Self::Target {
383    self.0.get_or_init(Self::Target::default)
384  }
385}
386
387#[cfg(not(target_os = "windows"))]
388static CLIENT: CellWrapper<BackendClient> = CellWrapper::new();
389
390/// Represents an inflight request to the backend for NFC tags on the reader
391/// You can spawn an entity with this component to poll the request:
392///
393/// # Example
394/// ```
395/// #[derive(Component, Deref, DerefMut)]
396/// struct MyNfcTagRequest(NfcTagRequestComponent);
397/// fn nfc_system(mut commands: Commands, mut tags_request: Query<(&mut MyNfcTagRequest, Entity)>) {
398///   for (mut tags_request, id) in &mut tags_request {
399///     if let Some(tag) = tags_request.poll() {
400///       println!("Got a response! {tag:?}");
401///       commands.entity(id).despawn();
402///     }
403///   }
404///   if tags_request.is_empty() {
405///     println!("Creating a new request...");
406///     commands.spawn(MyNfcTagRequest(NfcTagRequestComponent::new()));
407///   }
408/// }
409/// ```
410#[derive(Component)]
411#[cfg(not(target_os = "windows"))]
412pub struct NfcTagRequestComponent(Task<Result<Option<String>, RequestError>>);
413#[cfg(not(target_os = "windows"))]
414impl Default for NfcTagRequestComponent {
415  fn default() -> Self {
416    Self::new()
417  }
418}
419
420#[cfg(not(target_os = "windows"))]
421impl NfcTagRequestComponent {
422  /// Creates a new `NfcTagRequestComponent`
423  pub fn new() -> Self {
424    let pool = AsyncComputeTaskPool::get();
425    Self(pool.spawn(Compat::new(async move {
426      CLIENT
427        .send(RequestBody::GetNfcTag(BackendPlayer::P1))
428        .await
429        .and_then(|response_body| match response_body {
430          ResponseBody::NfcTag(tag_id) => Ok(tag_id),
431          body => Err(RequestError::UnexpectedResponse(body)),
432        })
433    })))
434  }
435  /// Check if this request has completed.
436  /// If it has, the return value will be `Some` with either the
437  /// assocation ID as a `String` or `None` if no tags were on the reader
438  pub fn poll(&mut self) -> Option<Result<Option<String>, RequestError>> {
439    future::block_on(future::poll_once(&mut self.0))
440  }
441}
442
443/// Represents an inflight request to the backend for the user associated with
444/// a particular NFC tag assocation id
445///
446/// You can spawn an entity with this component to poll the request:
447/// # Example
448/// ```
449/// #[derive(Component, Deref, DerefMut)]
450/// struct MyNfcTagRequest(NfcTagRequestComponent);
451/// #[derive(Component, Deref, DerefMut)]
452/// struct MyNfcUserRequest(NfcUserRequestComponent);
453/// fn nfc_system(
454///   mut commands: Commands,
455///   mut tags_request: Query<(&mut MyNfcTagRequest, Entity)>
456///   mut users_request: Query<(&mut MyNfcUserRequest, Entity)>
457/// ) {
458///   for (mut tags_request, id) in &mut tags_request {
459///     if let Some(tag) = tags_request.poll() {
460///       println!("Got a response! {tag:?}");
461///       commands.entity(id).despawn();
462///       if let Ok(Some(tag_id)) = tag {
463///         commands.spawn(MyNfcUserRequest(NfcUserRequestComponent::new(tag));
464///       }
465///     }
466///   }
467///   for (mut users_request, id) in &mut users_request {
468///     if let Some(user) = users_request.poll() {
469///       println!("Got a response! {user:?}");
470///       commands.entity(id).despawn();
471///       if let Ok(user) = user {
472///         println!("Username is: {}", user["uid"].as_str().unwrap());
473///       }
474///     }
475///   }
476///   if tags_request.is_empty() && users_request.is_empty() {
477///     println!("Creating a new request...");
478///     commands.spawn(MyNfcTagRequest(NfcTagRequestComponent::new()));
479///   }
480/// }
481/// ```
482#[derive(Component)]
483#[cfg(not(target_os = "windows"))]
484pub struct NfcUserRequestComponent(Task<Result<Map<String, Value>, RequestError>>);
485
486#[cfg(not(target_os = "windows"))]
487impl NfcUserRequestComponent {
488  /// Creates a new `NfcUserRequestComponent`
489  pub fn new(association_id: String) -> Self {
490    let pool = AsyncComputeTaskPool::get();
491    Self(pool.spawn(Compat::new(async move {
492      CLIENT
493        .send(RequestBody::GetNfcUser(association_id))
494        .await
495        .and_then(|response_body| match response_body {
496          ResponseBody::NfcUser(value) => Ok(value),
497          body => Err(RequestError::UnexpectedResponse(body)),
498        })
499    })))
500  }
501
502  /// Check if this request has completed.
503  /// If it has, the return value will be a `Result` with either list of the
504  /// user's attributes or a [`RequestError`] explaining why the request failed
505  pub fn poll(&mut self) -> Option<Result<Map<String, Value>, RequestError>> {
506    future::block_on(future::poll_once(&mut self.0))
507  }
508}