Skip to main content

shuyi_assets/keymap/
action_set.rs

1//! What a game's input actions are to the input mechanism (ticket 1106):
2//! [`ActionSet`], implemented by the game's own action enum, and
3//! [`UiAction`], the few actions the generic widgets and screens react to.
4//! `UiAction` is an action set too, ready-made for a game that plays with
5//! the six moves alone (ADR-0036).
6//!
7//! Generic over the game: nothing here names one game's actions.
8use core::fmt::Debug;
9use core::hash::Hash;
10
11use super::{Chord, Key};
12
13/// What the generic widgets (a menu), the Key bindings screen and the debug
14/// hook react to. A game maps some of its actions onto these with
15/// [`ActionSet::ui`]; the rest are its own business.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub enum UiAction {
18    /// Move up (a menu's focus, a cursor).
19    Up,
20    /// Move down.
21    Down,
22    /// Move left.
23    Left,
24    /// Move right.
25    Right,
26    /// Confirm / select.
27    Confirm,
28    /// Cancel / back.
29    Cancel,
30    /// Open the developer's debug screens.
31    Debug,
32}
33
34/// Everything a player can ask for in one game: the game's action enum.
35/// The keymap file, the keymap, held keys and repeat, the player's saved
36/// bindings and help text are all built on it, never on one game's
37/// actions.
38pub trait ActionSet: Copy + Ord + Eq + Hash + Debug + 'static {
39    /// Which actions may share a key or button: a key is on at most one
40    /// action of each group (game ADR-0068). `()` for a game where no two
41    /// actions share one.
42    type Group: Copy + Ord + Eq + Debug;
43
44    /// Every action, in the order the game lists them (the order a repair
45    /// of the saved bindings keeps the first of two claims in).
46    const ALL: &'static [Self];
47
48    /// The name used in keymap files and the saved bindings.
49    fn name(self) -> &'static str;
50
51    /// Looks an action up by its [`name`](Self::name) (exact match).
52    fn from_name(name: &str) -> Option<Self> {
53        Self::ALL.iter().copied().find(|a| a.name() == name)
54    }
55
56    /// Whether holding its key or button re-emits it.
57    fn is_repeatable(self) -> bool;
58
59    /// Whether the player must always keep at least one key (and one
60    /// button) on it.
61    fn is_required(self) -> bool;
62
63    /// Whether the player can rebind it. One that can't keeps the keymap
64    /// file's keys and has no controller button.
65    fn is_rebindable(self) -> bool;
66
67    /// Its [`Group`](Self::Group).
68    fn group(self) -> Self::Group;
69
70    /// What a generic widget or screen takes it for, if anything.
71    fn ui(self) -> Option<UiAction>;
72
73    /// The keys every keymap has on top of its own bindings, which the
74    /// player can't move or remove (this game: plain `Escape` cancels).
75    /// Shown after an action's own keys in help text, but never counted as
76    /// a key of its own. A game with none returns `&[]`.
77    fn fixed_keys() -> &'static [(Chord, Self)];
78
79    /// The first action that [`ui`](Self::ui) maps to `ui`, if any.
80    fn for_ui(ui: UiAction) -> Option<Self> {
81        Self::ALL.iter().copied().find(|a| a.ui() == Some(ui))
82    }
83}
84
85/// The keys every keymap of [`UiAction`]s has on top of its own: plain
86/// `Escape` cancels.
87const UI_FIXED_KEYS: [(Chord, UiAction); 1] = [(Chord::plain(Key::Escape), UiAction::Cancel)];
88
89/// The ready-made action set of a game that plays with the six moves: its
90/// keymap file names `Up`, `Down`, `Left`, `Right`, `Confirm` and `Cancel`,
91/// every one required and rebindable. [`UiAction::Debug`] is no action of
92/// the set: a game with a debug key writes its own action enum. A
93/// `UiAction` added later stays out of [`ALL`](ActionSet::ALL) too, so no
94/// game's keymap file must name it (ADR-0036).
95impl ActionSet for UiAction {
96    /// No two actions share a key.
97    type Group = ();
98
99    const ALL: &'static [Self] = &[
100        Self::Up,
101        Self::Down,
102        Self::Left,
103        Self::Right,
104        Self::Confirm,
105        Self::Cancel,
106    ];
107
108    fn name(self) -> &'static str {
109        match self {
110            Self::Up => "Up",
111            Self::Down => "Down",
112            Self::Left => "Left",
113            Self::Right => "Right",
114            Self::Confirm => "Confirm",
115            Self::Cancel => "Cancel",
116            Self::Debug => "Debug",
117        }
118    }
119
120    /// The four moves repeat while held.
121    fn is_repeatable(self) -> bool {
122        matches!(self, Self::Up | Self::Down | Self::Left | Self::Right)
123    }
124
125    /// The six must keep a key: the game can't be played without one.
126    fn is_required(self) -> bool {
127        self != Self::Debug
128    }
129
130    fn is_rebindable(self) -> bool {
131        self != Self::Debug
132    }
133
134    fn group(self) {}
135
136    fn ui(self) -> Option<UiAction> {
137        Some(self)
138    }
139
140    fn fixed_keys() -> &'static [(Chord, Self)] {
141        &UI_FIXED_KEYS
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::keymap::KeymapDef;
149
150    /// A keymap file of the six moves, and `extra` lines in its layout and
151    /// its pad.
152    fn source(extra: &str) -> String {
153        format!(
154            r#"(
155    layouts: [(
156        id: "Arrows",
157        name: "layout.arrows",
158        keys: {{
159            "Up": ["Up"], "Down": ["Down"], "Left": ["Left"], "Right": ["Right"],
160            "Confirm": ["Enter"], "Cancel": ["Backspace"],{extra}
161        }},
162    )],
163    layout_picker: {{ "Up": ["Up"], "Down": ["Down"], "Confirm": ["Enter"] }},
164    pad: {{
165        "Up": ["DpadUp"], "Down": ["DpadDown"], "Left": ["DpadLeft"],
166        "Right": ["DpadRight"], "Confirm": ["South"], "Cancel": ["East"],
167    }},
168    targets: {{}},
169    repeat: (delay_ms: 300, interval_ms: 70),
170    stick: (press_percent: 50, release_percent: 35),
171)"#
172        )
173    }
174
175    #[test]
176    fn the_six_moves_are_the_set_and_debug_is_not() {
177        assert_eq!(
178            UiAction::ALL,
179            [
180                UiAction::Up,
181                UiAction::Down,
182                UiAction::Left,
183                UiAction::Right,
184                UiAction::Confirm,
185                UiAction::Cancel,
186            ]
187        );
188        for &action in UiAction::ALL {
189            assert_eq!(UiAction::from_name(action.name()), Some(action));
190            assert_eq!(action.ui(), Some(action));
191            assert_eq!(UiAction::for_ui(action), Some(action));
192            assert!(action.is_required() && action.is_rebindable());
193        }
194        let repeating: Vec<UiAction> = UiAction::ALL
195            .iter()
196            .copied()
197            .filter(|a| a.is_repeatable())
198            .collect();
199        assert_eq!(
200            repeating,
201            [
202                UiAction::Up,
203                UiAction::Down,
204                UiAction::Left,
205                UiAction::Right
206            ]
207        );
208        assert_eq!(UiAction::Debug.name(), "Debug");
209        assert_eq!(UiAction::from_name("Debug"), None);
210        assert_eq!(UiAction::for_ui(UiAction::Debug), None);
211        assert!(!UiAction::Debug.is_required() && !UiAction::Debug.is_rebindable());
212        assert!(!UiAction::Debug.is_repeatable());
213        assert_eq!(
214            UiAction::fixed_keys(),
215            [(Chord::plain(Key::Escape), UiAction::Cancel)]
216        );
217    }
218
219    #[test]
220    fn a_keymap_file_of_the_six_moves_loads_and_one_naming_debug_is_refused() {
221        let keymap = KeymapDef::<UiAction>::from_source("keymap.ron", &source(""))
222            .unwrap_or_else(|e| panic!("{e:?}"));
223        assert_eq!(
224            keymap
225                .first_layout()
226                .map(|l| l.name().to_owned())
227                .as_deref(),
228            Some("Arrows")
229        );
230        let errors =
231            KeymapDef::<UiAction>::from_source("keymap.ron", &source(r#" "Debug": ["F3"],"#))
232                .unwrap_err();
233        assert!(
234            errors
235                .iter()
236                .any(|e| e.to_string().contains("unknown action \"Debug\"")),
237            "{errors:?}"
238        );
239    }
240}