Skip to main content

limnus_gamepad/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/limnus
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use fixstr::FixStr;
6use limnus_app::prelude::{App, Plugin};
7use limnus_message::Messages;
8use limnus_message::prelude::Message;
9use limnus_resource::prelude::Resource;
10use std::collections::HashMap;
11use tracing::{debug, error, trace};
12
13#[repr(usize)]
14#[derive(Debug, Clone, Eq, PartialEq, Copy)]
15pub enum Button {
16    // Right side pad
17    South,
18    East,
19    North,
20    West,
21
22    // Triggers
23    LeftTrigger,
24    LeftTrigger2,
25    RightTrigger,
26    RightTrigger2,
27
28    // Menu Buttons
29    Select,
30    Start,
31    Mode, // Xbox Button, PS button, etc
32
33    // Sticks
34    LeftThumb,
35    RightThumb,
36
37    // D-Pad (usually on the left side)
38    DPadUp,
39    DPadDown,
40    DPadLeft,
41    DPadRight,
42}
43
44#[repr(usize)]
45#[derive(Debug, Clone, Eq, PartialEq, Copy)]
46pub enum Axis {
47    LeftStickX,
48    LeftStickY,
49    RightStickX,
50    RightStickY,
51}
52
53pub type GamePadId = usize;
54
55pub type AxisValueType = f32;
56pub type ButtonValueType = f32;
57
58/// Messages sent when gamepad state changes
59#[derive(Debug, Message)]
60pub enum GamepadMessage {
61    Connected(GamePadId, String),
62    Disconnected(GamePadId),
63    Activated(GamePadId), // Sent when first button is pressed
64    ButtonChanged(GamePadId, Button, ButtonValueType),
65    AxisChanged(GamePadId, Axis, AxisValueType),
66}
67
68/// Represents a single gamepad's state
69#[derive(Default, Debug, Clone)]
70pub struct Gamepad {
71    pub axis: [AxisValueType; 4],
72    pub buttons: [ButtonValueType; 17],
73    pub name: FixStr<64>,
74    pub id: GamePadId,
75    pub is_active: bool,
76}
77
78impl Gamepad {
79    #[must_use]
80    pub fn new(id: GamePadId, name: &str) -> Self {
81        let truncated_name: String = name.chars().take(32).collect();
82        Self {
83            axis: [0.0; 4],
84            buttons: [0.0; 17],
85            name: FixStr::new(&truncated_name).expect("gamepad name too long"), // TODO: Make a better solution for this
86            id,
87            is_active: false,
88        }
89    }
90
91    /// Gets the button state as a boolean
92    #[must_use]
93    pub fn is_pressed(&self, button: Button) -> bool {
94        self.buttons[button as usize] > 0.1
95    }
96
97    #[must_use]
98    pub const fn axis(&self, axis: Axis) -> AxisValueType {
99        self.axis[axis as usize]
100    }
101
102    #[must_use]
103    pub const fn button(&self, button: Button) -> ButtonValueType {
104        self.buttons[button as usize]
105    }
106}
107
108#[derive(Debug, Resource)]
109pub struct Gamepads {
110    gamepads: HashMap<GamePadId, Gamepad>,
111}
112
113impl Default for Gamepads {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl Gamepads {
120    /// Creates a new `GamePad` instance
121    ///
122    /// # Arguments
123    /// * `id` - Unique identifier for this gamepad
124    /// * `name` - Human-readable name of the gamepad
125    #[must_use]
126    pub fn new() -> Self {
127        Self {
128            gamepads: HashMap::new(),
129        }
130    }
131    pub fn connected(&mut self, id: GamePadId, name: &str, queue: &mut Messages<GamepadMessage>) {
132        debug!(id=%id, name=name, "connected gamepad");
133        self.gamepads.insert(id, Gamepad::new(id, name));
134        queue.send(GamepadMessage::Connected(id, name.to_string()));
135    }
136
137    pub fn disconnected(&mut self, id: GamePadId, queue: &mut Messages<GamepadMessage>) {
138        if let Some(existing) = self.gamepads.remove(&id) {
139            debug!(id=%id, name=?existing.name, "disconnected gamepad");
140            queue.send(GamepadMessage::Disconnected(id));
141        } else {
142            error!(id=%id, "gamepad not found");
143        }
144    }
145
146    #[must_use]
147    pub fn gamepad(&self, id: GamePadId) -> Option<&Gamepad> {
148        self.gamepads.get(&id)
149    }
150
151    /// Gets the axis value for a gamepad
152    #[must_use]
153    pub fn axis(&self, id: GamePadId, axis: Axis) -> Option<AxisValueType> {
154        self.gamepad(id).map(|pad| pad.axis[axis as usize])
155    }
156
157    /// Gets the button value for a gamepad
158    #[must_use]
159    pub fn button(&self, id: GamePadId, button: Button) -> Option<ButtonValueType> {
160        self.gamepad(id).map(|pad| pad.buttons[button as usize])
161    }
162
163    pub fn iter_active(&self) -> impl Iterator<Item = &Gamepad> {
164        self.gamepads.values().filter(|gamepad| gamepad.is_active)
165    }
166
167    pub fn iter(&self) -> impl Iterator<Item = &Gamepad> {
168        self.gamepads.values()
169    }
170
171    pub fn set_axis(
172        &mut self,
173        id: GamePadId,
174        axis: Axis,
175        value: AxisValueType,
176        queue: &mut Messages<GamepadMessage>,
177    ) -> Option<()> {
178        trace!(id=?id, axis=?axis, value=?value, "set axis");
179        let gamepad = self.gamepads.get_mut(&id)?;
180
181        queue.send(GamepadMessage::AxisChanged(id, axis, value));
182        gamepad.axis[axis as usize] = value;
183
184        Some(())
185    }
186
187    pub fn set_button(
188        &mut self,
189        id: GamePadId,
190        button: Button,
191        value: ButtonValueType,
192        queue: &mut Messages<GamepadMessage>,
193    ) -> Option<()> {
194        trace!(id=?id, button=?button, value=?value, "set button");
195
196        let gamepad = self.gamepads.get_mut(&id)?;
197
198        if !gamepad.is_active && value > 0.1 {
199            debug!(id=%id, button=?button, name=%gamepad.name, "gamepad activated");
200            queue.send(GamepadMessage::Activated(id));
201            gamepad.is_active = true;
202        }
203
204        queue.send(GamepadMessage::ButtonChanged(id, button, value));
205        gamepad.buttons[button as usize] = value;
206        Some(())
207    }
208
209    /// Gets the name of a gamepad
210    #[must_use]
211    pub fn name(&self, id: GamePadId) -> Option<&str> {
212        self.gamepad(id).map(|pad| pad.name.as_str())
213    }
214}
215
216pub struct GamepadResourcePlugin;
217
218impl Plugin for GamepadResourcePlugin {
219    fn build(&self, app: &mut App) {
220        app.insert_resource(Gamepads::new());
221        app.create_message_type::<GamepadMessage>();
222    }
223}