use std::collections::BTreeMap;
use std::time::Duration;
use bevy::input::gamepad::{
GamepadAxis, GamepadButton, GamepadConnection, GamepadConnectionEvent, GamepadEvent,
GamepadRumbleIntensity, GamepadRumbleRequest,
};
use bevy::log::warn;
use bevy::prelude::*;
use serde::Serialize;
use ts_rs::TS;
use crate::event::ReactEvents;
use crate::reconcile::OpApplyStats;
use crate::request::Request;
use crate::{react_event, react_message, react_request};
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub enum GamepadButtonName {
South,
East,
North,
West,
C,
Z,
LeftTrigger,
LeftTrigger2,
RightTrigger,
RightTrigger2,
Select,
Start,
Mode,
LeftThumb,
RightThumb,
DPadUp,
DPadDown,
DPadLeft,
DPadRight,
Other(u8),
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub enum GamepadAxisName {
LeftStickX,
LeftStickY,
LeftZ,
RightStickX,
RightStickY,
RightZ,
Other(u8),
}
impl From<GamepadButton> for GamepadButtonName {
fn from(button: GamepadButton) -> Self {
match button {
GamepadButton::South => Self::South,
GamepadButton::East => Self::East,
GamepadButton::North => Self::North,
GamepadButton::West => Self::West,
GamepadButton::C => Self::C,
GamepadButton::Z => Self::Z,
GamepadButton::LeftTrigger => Self::LeftTrigger,
GamepadButton::LeftTrigger2 => Self::LeftTrigger2,
GamepadButton::RightTrigger => Self::RightTrigger,
GamepadButton::RightTrigger2 => Self::RightTrigger2,
GamepadButton::Select => Self::Select,
GamepadButton::Start => Self::Start,
GamepadButton::Mode => Self::Mode,
GamepadButton::LeftThumb => Self::LeftThumb,
GamepadButton::RightThumb => Self::RightThumb,
GamepadButton::DPadUp => Self::DPadUp,
GamepadButton::DPadDown => Self::DPadDown,
GamepadButton::DPadLeft => Self::DPadLeft,
GamepadButton::DPadRight => Self::DPadRight,
GamepadButton::Other(n) => Self::Other(n),
}
}
}
impl From<GamepadAxis> for GamepadAxisName {
fn from(axis: GamepadAxis) -> Self {
match axis {
GamepadAxis::LeftStickX => Self::LeftStickX,
GamepadAxis::LeftStickY => Self::LeftStickY,
GamepadAxis::LeftZ => Self::LeftZ,
GamepadAxis::RightStickX => Self::RightStickX,
GamepadAxis::RightStickY => Self::RightStickY,
GamepadAxis::RightZ => Self::RightZ,
GamepadAxis::Other(n) => Self::Other(n),
}
}
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct GamepadConnectedData {
gamepad: u32,
name: String,
vendor_id: Option<u16>,
product_id: Option<u16>,
}
#[react_event(name = "gamepadConnected")]
pub struct GamepadConnected(pub GamepadConnectedData);
#[derive(Serialize, TS)]
pub struct GamepadDisconnectedData {
gamepad: u32,
}
#[react_event(name = "gamepadDisconnected")]
pub struct GamepadDisconnected(pub GamepadDisconnectedData);
#[derive(Serialize, TS)]
pub struct GamepadButtonChange {
gamepad: u32,
button: GamepadButtonName,
pressed: bool,
value: f32,
}
#[derive(Serialize, TS)]
pub struct GamepadAxisChange {
gamepad: u32,
axis: GamepadAxisName,
value: f32,
}
#[derive(Serialize, TS)]
pub struct GamepadInputData {
buttons: Vec<GamepadButtonChange>,
axes: Vec<GamepadAxisChange>,
}
#[react_event(name = "gamepadInput")]
pub struct GamepadInputEvent(pub GamepadInputData);
#[react_message(name = "gamepad.rumble")]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub struct GamepadRumble {
gamepad: u32,
duration: f32,
strong_motor: f32,
weak_motor: f32,
}
#[react_message(name = "gamepad.stopRumble")]
pub struct GamepadStopRumble {
gamepad: u32,
}
#[react_request(name = "gamepad.getAll", response = Vec<GamepadConnectedData>)]
pub struct GamepadGetAll;
struct PadInfo {
entity: Entity,
name: String,
vendor_id: Option<u16>,
product_id: Option<u16>,
}
#[derive(Resource, Default)]
pub(crate) struct GamepadRegistry {
next_id: u32,
pads: BTreeMap<u32, PadInfo>,
}
impl GamepadRegistry {
fn connect(
&mut self,
entity: Entity,
name: String,
vendor_id: Option<u16>,
product_id: Option<u16>,
) -> (u32, bool) {
if let Some(id) = self.id_for(entity) {
let pad = self.pads.get_mut(&id).expect("id_for returned a live id");
pad.name = name;
pad.vendor_id = vendor_id;
pad.product_id = product_id;
return (id, false);
}
let id = self.next_id;
self.next_id += 1;
self.pads.insert(
id,
PadInfo {
entity,
name,
vendor_id,
product_id,
},
);
(id, true)
}
fn disconnect(&mut self, entity: Entity) -> Option<u32> {
let id = self.id_for(entity)?;
self.pads.remove(&id);
Some(id)
}
fn id_for(&self, entity: Entity) -> Option<u32> {
self.iter()
.find(|(_, pad)| pad.entity == entity)
.map(|(id, _)| id)
}
fn entity_for(&self, id: u32) -> Option<Entity> {
self.pads.get(&id).map(|pad| pad.entity)
}
fn iter(&self) -> impl Iterator<Item = (u32, &PadInfo)> {
self.pads.iter().map(|(id, pad)| (*id, pad))
}
}
fn connected_data(id: u32, pad: &PadInfo) -> GamepadConnectedData {
GamepadConnectedData {
gamepad: id,
name: pad.name.clone(),
vendor_id: pad.vendor_id,
product_id: pad.product_id,
}
}
pub(crate) fn collect_gamepad_events(
stats: Res<OpApplyStats>,
mut connections: MessageReader<GamepadConnectionEvent>,
mut reader: MessageReader<GamepadEvent>,
mut registry: ResMut<GamepadRegistry>,
events: ReactEvents,
mut announced: Local<bool>,
) {
let live = stats.applied_count > 0;
if connections.is_empty() && reader.is_empty() && (*announced || !live) {
return;
}
for conn in connections.read() {
match &conn.connection {
GamepadConnection::Connected {
name,
vendor_id,
product_id,
} => {
let (id, is_new) =
registry.connect(conn.gamepad, name.clone(), *vendor_id, *product_id);
if is_new && live && *announced {
events.send(&GamepadConnected(connected_data(id, ®istry.pads[&id])));
}
}
GamepadConnection::Disconnected => {
if let Some(id) = registry.disconnect(conn.gamepad)
&& live
&& *announced
{
events.send(&GamepadDisconnected(GamepadDisconnectedData {
gamepad: id,
}));
}
}
}
}
let mut buttons = Vec::new();
let mut axes = Vec::new();
for ev in reader.read() {
match ev {
GamepadEvent::Connection(_) => {}
GamepadEvent::Button(change) => {
if live && let Some(id) = registry.id_for(change.entity) {
buttons.push(GamepadButtonChange {
gamepad: id,
button: change.button.into(),
pressed: change.state.is_pressed(),
value: change.value,
});
}
}
GamepadEvent::Axis(change) => {
if live && let Some(id) = registry.id_for(change.entity) {
axes.push(GamepadAxisChange {
gamepad: id,
axis: change.axis.into(),
value: change.value,
});
}
}
}
}
if live && !*announced {
*announced = true;
for (id, pad) in registry.iter() {
events.send(&GamepadConnected(connected_data(id, pad)));
}
}
if !buttons.is_empty() || !axes.is_empty() {
events.send(&GamepadInputEvent(GamepadInputData { buttons, axes }));
}
}
pub(crate) fn handle_gamepad_get_all(
req: On<Request<GamepadGetAll>>,
registry: Res<GamepadRegistry>,
) {
req.respond(
registry
.iter()
.map(|(id, pad)| connected_data(id, pad))
.collect::<Vec<_>>(),
);
}
pub(crate) fn on_rumble(
on: On<GamepadRumble>,
registry: Res<GamepadRegistry>,
mut rumble: MessageWriter<GamepadRumbleRequest>,
) {
let msg = on.event();
let Some(entity) = registry.entity_for(msg.gamepad) else {
warn!("gamepad.rumble: unknown gamepad id {}", msg.gamepad);
return;
};
rumble.write(GamepadRumbleRequest::Add {
gamepad: entity,
duration: Duration::from_secs_f32(msg.duration.max(0.0) / 1000.0),
intensity: GamepadRumbleIntensity {
strong_motor: msg.strong_motor.clamp(0.0, 1.0),
weak_motor: msg.weak_motor.clamp(0.0, 1.0),
},
});
}
pub(crate) fn on_stop_rumble(
on: On<GamepadStopRumble>,
registry: Res<GamepadRegistry>,
mut rumble: MessageWriter<GamepadRumbleRequest>,
) {
let msg = on.event();
let Some(entity) = registry.entity_for(msg.gamepad) else {
warn!("gamepad.stopRumble: unknown gamepad id {}", msg.gamepad);
return;
};
rumble.write(GamepadRumbleRequest::Stop { gamepad: entity });
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ReactAppExt;
use crate::bridge::{OutboundResource, OutboundSender};
use crate::message::{ReactMessage, ReactRegistry};
use crate::protocol::{outbound::Outbound, outbound::ResponseResult};
use crate::request::{RawRequest, ReactRequestRegistry};
use bevy::ecs::world::CommandQueue;
use bevy::input::ButtonState;
use bevy::input::gamepad::{
GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadConnectionEvent,
};
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
fn test_app() -> (App, UnboundedReceiver<Outbound>) {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_message::<GamepadEvent>();
app.add_message::<GamepadConnectionEvent>();
app.init_resource::<OpApplyStats>();
app.init_resource::<GamepadRegistry>();
let (tx, rx) = unbounded_channel::<Outbound>();
app.insert_resource(OutboundResource(tx));
app.add_systems(Update, collect_gamepad_events);
(app, rx)
}
fn open_gate(app: &mut App) {
app.world_mut().resource_mut::<OpApplyStats>().applied_count = 1;
}
fn write_connect(app: &mut App, entity: Entity, name: &str) {
app.world_mut().write_message(GamepadConnectionEvent::new(
entity,
GamepadConnection::Connected {
name: name.to_string(),
vendor_id: Some(0x054c),
product_id: Some(0x0ce6),
},
));
}
fn write_disconnect(app: &mut App, entity: Entity) {
app.world_mut().write_message(GamepadConnectionEvent::new(
entity,
GamepadConnection::Disconnected,
));
}
fn write_button(app: &mut App, entity: Entity, button: GamepadButton, value: f32) {
let state = if value > 0.0 {
ButtonState::Pressed
} else {
ButtonState::Released
};
app.world_mut()
.write_message(GamepadEvent::Button(GamepadButtonChangedEvent::new(
entity, button, state, value,
)));
}
fn write_axis(app: &mut App, entity: Entity, axis: GamepadAxis, value: f32) {
app.world_mut()
.write_message(GamepadEvent::Axis(GamepadAxisChangedEvent::new(
entity, axis, value,
)));
}
fn expect_event(rx: &mut UnboundedReceiver<Outbound>, expected: &str) -> serde_json::Value {
match rx
.try_recv()
.unwrap_or_else(|_| panic!("a {expected} event"))
{
Outbound::Event { name, value } => {
assert_eq!(name, expected);
value
}
other => panic!("expected Outbound::Event, got {other:?}"),
}
}
#[test]
fn connect_gates_until_first_batch_then_announces() {
let (mut app, mut rx) = test_app();
let pad_a = app.world_mut().spawn_empty().id();
let pad_b = app.world_mut().spawn_empty().id();
write_connect(&mut app, pad_a, "Pad A");
app.update();
assert!(rx.try_recv().is_err(), "gated until a batch has applied");
open_gate(&mut app);
app.update();
let value = expect_event(&mut rx, "gamepadConnected");
assert_eq!(value["gamepad"], 0);
assert_eq!(value["name"], "Pad A");
assert_eq!(value["vendorId"], 0x054c);
assert_eq!(value["productId"], 0x0ce6);
assert!(rx.try_recv().is_err(), "no double announce");
write_connect(&mut app, pad_b, "Pad B");
app.update();
let value = expect_event(&mut rx, "gamepadConnected");
assert_eq!(value["gamepad"], 1);
assert_eq!(value["name"], "Pad B");
write_disconnect(&mut app, pad_a);
app.update();
let value = expect_event(&mut rx, "gamepadDisconnected");
assert_eq!(value["gamepad"], 0);
write_connect(&mut app, pad_a, "Pad A");
app.update();
let value = expect_event(&mut rx, "gamepadConnected");
assert_eq!(value["gamepad"], 2);
}
#[test]
fn input_coalesces_into_one_event_per_frame() {
let (mut app, mut rx) = test_app();
let pad = app.world_mut().spawn_empty().id();
open_gate(&mut app);
write_connect(&mut app, pad, "Pad");
app.update();
expect_event(&mut rx, "gamepadConnected");
write_button(&mut app, pad, GamepadButton::South, 1.0);
write_button(&mut app, pad, GamepadButton::DPadUp, 0.0);
write_axis(&mut app, pad, GamepadAxis::LeftStickY, 0.42);
app.update();
let value = expect_event(&mut rx, "gamepadInput");
let buttons = value["buttons"].as_array().expect("buttons array");
assert_eq!(buttons.len(), 2);
assert_eq!(buttons[0]["gamepad"], 0);
assert_eq!(buttons[0]["button"], "south");
assert_eq!(buttons[0]["pressed"], true);
assert_eq!(buttons[0]["value"], 1.0);
assert_eq!(buttons[1]["button"], "dPadUp");
assert_eq!(buttons[1]["pressed"], false);
let axes = value["axes"].as_array().expect("axes array");
assert_eq!(axes.len(), 1);
assert_eq!(axes[0]["axis"], "leftStickY");
assert!((axes[0]["value"].as_f64().unwrap() - 0.42).abs() < 1e-6);
assert!(rx.try_recv().is_err(), "one gamepadInput per frame");
app.update();
assert!(rx.try_recv().is_err(), "quiet frame sends nothing");
}
#[test]
fn other_button_serializes_externally_tagged() {
let (mut app, mut rx) = test_app();
let pad = app.world_mut().spawn_empty().id();
open_gate(&mut app);
write_connect(&mut app, pad, "Pad");
app.update();
expect_event(&mut rx, "gamepadConnected");
write_button(&mut app, pad, GamepadButton::Other(7), 1.0);
app.update();
let value = expect_event(&mut rx, "gamepadInput");
assert_eq!(
value["buttons"][0]["button"],
serde_json::json!({"other": 7})
);
}
#[test]
fn duplicate_connect_same_entity_keeps_id_and_announces_once() {
let (mut app, mut rx) = test_app();
let pad = app.world_mut().spawn_empty().id();
write_connect(&mut app, pad, "Pad");
open_gate(&mut app);
app.update();
let value = expect_event(&mut rx, "gamepadConnected");
assert_eq!(value["gamepad"], 0);
write_connect(&mut app, pad, "Pad");
app.update();
assert!(rx.try_recv().is_err(), "duplicate must not re-announce");
write_button(&mut app, pad, GamepadButton::South, 1.0);
app.update();
let value = expect_event(&mut rx, "gamepadInput");
assert_eq!(value["buttons"][0]["gamepad"], 0);
}
#[test]
fn dual_stream_connect_registers_once() {
let (mut app, mut rx) = test_app();
let pad = app.world_mut().spawn_empty().id();
open_gate(&mut app);
app.update();
let conn = GamepadConnectionEvent::new(
pad,
GamepadConnection::Connected {
name: "Pad".to_string(),
vendor_id: None,
product_id: None,
},
);
app.world_mut()
.write_message(GamepadEvent::Connection(conn.clone()));
app.world_mut().write_message(conn);
app.update();
let value = expect_event(&mut rx, "gamepadConnected");
assert_eq!(value["gamepad"], 0);
assert!(rx.try_recv().is_err(), "exactly one announce");
}
#[test]
fn unknown_entity_input_is_dropped() {
let (mut app, mut rx) = test_app();
let stray = app.world_mut().spawn_empty().id();
open_gate(&mut app);
app.update();
write_button(&mut app, stray, GamepadButton::South, 1.0);
app.update();
assert!(rx.try_recv().is_err());
}
#[test]
fn idle_frame_sends_nothing() {
let (mut app, mut rx) = test_app();
app.update();
assert!(rx.try_recv().is_err());
}
fn dispatch(app: &mut App, name: &str, value: serde_json::Value) {
app.world_mut()
.resource_scope(|world, registry: Mut<ReactRegistry>| {
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, world);
registry.dispatch(
ReactMessage {
name: name.into(),
value,
},
&mut commands,
);
queue.apply(world);
});
}
fn rumble_app() -> (App, Entity) {
let mut app = App::new();
app.add_message::<GamepadRumbleRequest>();
let pad = app.world_mut().spawn_empty().id();
let mut registry = GamepadRegistry::default();
registry.connect(pad, "Pad".into(), None, None);
app.insert_resource(registry);
app.add_react_handler(on_rumble);
app.add_react_handler(on_stop_rumble);
(app, pad)
}
fn drain_rumble(app: &mut App) -> Vec<GamepadRumbleRequest> {
app.world_mut()
.resource_mut::<Messages<GamepadRumbleRequest>>()
.drain()
.collect()
}
#[test]
fn rumble_maps_id_to_entity_and_writes_request() {
let (mut app, pad) = rumble_app();
dispatch(
&mut app,
"gamepad.rumble",
serde_json::json!({
"gamepad": 0,
"duration": 500.0,
"strongMotor": 1.5,
"weakMotor": 0.25,
}),
);
match drain_rumble(&mut app).as_slice() {
[
GamepadRumbleRequest::Add {
gamepad,
intensity,
duration,
},
] => {
assert_eq!(*gamepad, pad);
assert_eq!(*duration, Duration::from_millis(500));
assert_eq!(intensity.strong_motor, 1.0, "clamped to 0..=1");
assert_eq!(intensity.weak_motor, 0.25);
}
other => panic!("expected one Add request, got {} items", other.len()),
}
dispatch(
&mut app,
"gamepad.stopRumble",
serde_json::json!({"gamepad": 0}),
);
match drain_rumble(&mut app).as_slice() {
[GamepadRumbleRequest::Stop { gamepad }] => assert_eq!(*gamepad, pad),
other => panic!("expected one Stop request, got {} items", other.len()),
}
dispatch(
&mut app,
"gamepad.rumble",
serde_json::json!({
"gamepad": 99,
"duration": 100.0,
"strongMotor": 1.0,
"weakMotor": 1.0,
}),
);
assert!(
drain_rumble(&mut app).is_empty(),
"unknown id writes nothing"
);
}
fn dispatch_get_all(app: &mut App, tx: &OutboundSender, id: u64) {
app.world_mut()
.resource_scope(|world, registry: Mut<ReactRequestRegistry>| {
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, world);
registry.dispatch(
RawRequest {
id,
name: "gamepad.getAll".into(),
value: serde_json::Value::Null,
},
tx,
&mut commands,
);
queue.apply(world);
});
}
#[test]
fn get_all_request_returns_connected_pads() {
let mut app = App::new();
app.init_resource::<GamepadRegistry>();
app.add_react_request_handler(handle_gamepad_get_all);
let (tx, mut rx): (OutboundSender, UnboundedReceiver<Outbound>) = unbounded_channel();
dispatch_get_all(&mut app, &tx, 1);
match rx.try_recv() {
Ok(Outbound::Response {
id: 1,
result: ResponseResult::Ok { value },
}) => assert_eq!(value, serde_json::json!([])),
other => panic!("expected empty Ok response, got {other:?}"),
}
let pad = app.world_mut().spawn_empty().id();
app.world_mut().resource_mut::<GamepadRegistry>().connect(
pad,
"Pad".into(),
Some(0x045e),
None,
);
dispatch_get_all(&mut app, &tx, 2);
match rx.try_recv() {
Ok(Outbound::Response {
id: 2,
result: ResponseResult::Ok { value },
}) => {
let pads = value.as_array().expect("array response");
assert_eq!(pads.len(), 1);
assert_eq!(pads[0]["gamepad"], 0);
assert_eq!(pads[0]["name"], "Pad");
assert_eq!(pads[0]["vendorId"], 0x045e);
assert_eq!(pads[0]["productId"], serde_json::Value::Null);
}
other => panic!("expected Ok response, got {other:?}"),
}
}
#[test]
fn negative_rumble_duration_clamps_to_zero() {
let (mut app, _pad) = rumble_app();
dispatch(
&mut app,
"gamepad.rumble",
serde_json::json!({
"gamepad": 0,
"duration": -50.0,
"strongMotor": 0.5,
"weakMotor": 0.5,
}),
);
match drain_rumble(&mut app).as_slice() {
[GamepadRumbleRequest::Add { duration, .. }] => {
assert_eq!(*duration, Duration::ZERO);
}
other => panic!("expected one Add request, got {} items", other.len()),
}
}
}