Skip to main content

concinnity_core/components/
gamepad_map.rs

1// src/components/gamepad_map.rs
2
3use crate::components::GamepadButton;
4
5/// A rebindable gamepad action. Movement and look come from the sticks (with
6/// the d-pad as a digital movement fallback), so only the button-driven actions
7/// are rebindable; pause (Start) carries menu semantics and stays fixed, like
8/// Escape on the keyboard.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum GamepadAction {
11    /// Hold to move faster.
12    Sprint,
13    /// Jump.
14    Jump,
15    /// Interact with the entity under the cursor.
16    Interact,
17}
18
19impl GamepadAction {
20    /// Every rebindable action, in Controls-tab row order.
21    pub const ALL: [GamepadAction; 3] = [
22        GamepadAction::Sprint,
23        GamepadAction::Jump,
24        GamepadAction::Interact,
25    ];
26
27    /// The settings key string used in `setting:<key>:rebind` actions and the
28    /// engine settings registry. The `pad_` prefix distinguishes a button
29    /// capture row from a `key_*` keyboard capture row.
30    pub fn setting_key(self) -> &'static str {
31        match self {
32            GamepadAction::Sprint => "pad_sprint",
33            GamepadAction::Jump => "pad_jump",
34            GamepadAction::Interact => "pad_interact",
35        }
36    }
37
38    /// The action for a settings key string, or `None` if it is not a gamepad
39    /// rebind key.
40    pub fn from_setting_key(key: &str) -> Option<GamepadAction> {
41        GamepadAction::ALL
42            .into_iter()
43            .find(|a| a.setting_key() == key)
44    }
45}
46
47/// The canonical action -> gamepad button map. Persisted in the engine's
48/// controls settings and applied by the input sampling; carried live to
49/// consumers via [ControlsCommand](#controlscommand) on a rebind. Each field is
50/// `#[serde(default)]` so adding an action in a future build never invalidates
51/// an existing settings file.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
53pub struct GamepadMap {
54    /// Held to sprint while moving.
55    #[serde(default = "def_sprint")]
56    pub sprint: GamepadButton,
57    /// One-frame jump press.
58    #[serde(default = "def_jump")]
59    pub jump: GamepadButton,
60    /// One-frame interact press.
61    #[serde(default = "def_interact")]
62    pub interact: GamepadButton,
63}
64
65impl GamepadMap {
66    /// The default bindings: sprint on the left-stick click, jump on the bottom
67    /// face button, interact on the left face button.
68    pub const DEFAULT: GamepadMap = GamepadMap {
69        sprint: GamepadButton::LeftStick,
70        jump: GamepadButton::South,
71        interact: GamepadButton::West,
72    };
73
74    /// The button currently bound to an action.
75    pub fn get(self, action: GamepadAction) -> GamepadButton {
76        match action {
77            GamepadAction::Sprint => self.sprint,
78            GamepadAction::Jump => self.jump,
79            GamepadAction::Interact => self.interact,
80        }
81    }
82
83    /// Bind an action to a button directly (no conflict handling).
84    pub fn set(&mut self, action: GamepadAction, button: GamepadButton) {
85        match action {
86            GamepadAction::Sprint => self.sprint = button,
87            GamepadAction::Jump => self.jump = button,
88            GamepadAction::Interact => self.interact = button,
89        }
90    }
91
92    /// The action a button is bound to, or `None` if unbound. The map keeps
93    /// each button bound to at most one action (the invariant [rebind](#method.rebind)
94    /// maintains), so this is the unique holder.
95    pub fn action_for_button(self, button: GamepadButton) -> Option<GamepadAction> {
96        GamepadAction::ALL
97            .into_iter()
98            .find(|&a| self.get(a) == button)
99    }
100
101    /// Bind `action` to `new_button`, swapping with whichever action already
102    /// holds `new_button` so every action stays bound. Rebinding an action to
103    /// its own button is a no-op.
104    pub fn rebind(&mut self, action: GamepadAction, new_button: GamepadButton) {
105        let old_button = self.get(action);
106        if old_button == new_button {
107            return;
108        }
109        if let Some(other) = self.action_for_button(new_button)
110            && other != action
111        {
112            self.set(other, old_button);
113        }
114        self.set(action, new_button);
115    }
116}
117
118impl Default for GamepadMap {
119    fn default() -> Self {
120        Self::DEFAULT
121    }
122}
123
124fn def_sprint() -> GamepadButton {
125    GamepadMap::DEFAULT.sprint
126}
127fn def_jump() -> GamepadButton {
128    GamepadMap::DEFAULT.jump
129}
130fn def_interact() -> GamepadButton {
131    GamepadMap::DEFAULT.interact
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn setting_key_round_trips() {
140        for a in GamepadAction::ALL {
141            assert_eq!(GamepadAction::from_setting_key(a.setting_key()), Some(a));
142        }
143        assert_eq!(GamepadAction::from_setting_key("key_jump"), None);
144        assert_eq!(GamepadAction::from_setting_key("pad_nope"), None);
145    }
146
147    #[test]
148    fn get_set_cover_every_action_arm() {
149        let buttons = [
150            GamepadButton::North,
151            GamepadButton::East,
152            GamepadButton::RightShoulder,
153        ];
154        let mut m = GamepadMap::default();
155        for (a, b) in GamepadAction::ALL.into_iter().zip(buttons) {
156            m.set(a, b);
157        }
158        for (a, b) in GamepadAction::ALL.into_iter().zip(buttons) {
159            assert_eq!(m.get(a), b);
160        }
161    }
162
163    #[test]
164    fn rebind_to_free_button_just_sets_it() {
165        let mut m = GamepadMap::default();
166        m.rebind(GamepadAction::Jump, GamepadButton::North);
167        assert_eq!(m.jump, GamepadButton::North);
168        assert_eq!(m.interact, GamepadMap::DEFAULT.interact);
169    }
170
171    #[test]
172    fn rebind_to_own_button_is_a_noop() {
173        let mut m = GamepadMap::default();
174        m.rebind(GamepadAction::Jump, GamepadMap::DEFAULT.jump);
175        assert_eq!(m, GamepadMap::default());
176    }
177
178    #[test]
179    fn rebind_to_occupied_button_swaps() {
180        // Bind Jump to West, which Interact holds: they swap, so Interact
181        // inherits Jump's old button and every action stays bound.
182        let mut m = GamepadMap::default();
183        m.rebind(GamepadAction::Jump, GamepadButton::West);
184        assert_eq!(m.jump, GamepadButton::West);
185        assert_eq!(m.interact, GamepadMap::DEFAULT.jump);
186        for a in GamepadAction::ALL {
187            assert_eq!(m.action_for_button(m.get(a)), Some(a));
188        }
189    }
190
191    #[test]
192    fn missing_field_falls_back_to_default() {
193        // A settings file predating a field still loads, the missing field
194        // falling back through its `serde(default = "def_*")` helper.
195        let partial: GamepadMap = serde_json::from_str(r#"{"jump":"East"}"#).unwrap();
196        assert_eq!(partial.jump, GamepadButton::East);
197        assert_eq!(partial.sprint, GamepadMap::DEFAULT.sprint);
198        assert_eq!(partial.interact, GamepadMap::DEFAULT.interact);
199    }
200}