Skip to main content

shuyi_assets/
keymap.rs

1//! The keymap file format, generic over a game's [`ActionSet`] (game ADR-0015,
2//! game ADR-0031, game ADR-0034, tickets 1106 and 1107): [`Key`]s and [`Chord`]s,
3//! controller [`Button`]s and [`ButtonChord`]s (two buttons together), the
4//! named [`Layout`]s a file lists, and
5//! [`KeymapDef`], a validated keymap file: each layout's default keys, the
6//! keys that work before a layout is chosen, the default buttons, key-repeat
7//! timings and the sticks' thresholds.
8//!
9//! Nothing here names one game's actions or layouts: a game gives its
10//! actions as an [`ActionSet`] ([`action_set`]) and its file's text. As a
11//! format (ADR-0007): [`KeymapRon`].
12use crate::prelude::*;
13
14use alloc::collections::{BTreeMap, BTreeSet};
15use core::fmt;
16use core::marker::PhantomData;
17use core::str::FromStr;
18
19use serde::{Deserialize, Serialize};
20
21use crate::error::{ContentError, codes};
22use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, text};
23use crate::ron::parse_ron;
24
25pub mod action_set;
26#[cfg(any(test, feature = "sample-actions"))]
27pub mod sample;
28#[cfg(test)]
29mod tests;
30
31pub use action_set::{ActionSet, UiAction};
32
33/// Declares [`Key`] with its keymap-file names, keeping the variant list,
34/// [`Key::ALL`] and [`Key::name`] in one table.
35macro_rules! keys {
36    ($($variant:ident => $name:literal,)+) => {
37        /// A hardware-agnostic keyboard key. `app` translates platform key
38        /// codes to these; anything without a variant is ignored.
39        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
40        pub enum Key {
41            $(
42                #[doc = concat!("The `` ", $name, " `` key.")]
43                $variant,
44            )+
45        }
46
47        impl Key {
48            /// Every key, in declaration order.
49            pub const ALL: &'static [Key] = &[$(Key::$variant),+];
50
51            /// The name used in keymap files: lowercase letters (`h`), digits
52            /// (`0`), punctuation as itself (`;`, `,`, `\`), numpad keys
53            /// prefixed `Kp` (`Kp0`, `Kp+`), and capitalised names for the
54            /// rest (`Left`, `F12`).
55            pub fn name(self) -> &'static str {
56                match self {
57                    $(Key::$variant => $name,)+
58                }
59            }
60        }
61    };
62}
63
64keys! {
65    A => "a",
66    B => "b",
67    C => "c",
68    D => "d",
69    E => "e",
70    F => "f",
71    G => "g",
72    H => "h",
73    I => "i",
74    J => "j",
75    K => "k",
76    L => "l",
77    M => "m",
78    N => "n",
79    O => "o",
80    P => "p",
81    Q => "q",
82    R => "r",
83    S => "s",
84    T => "t",
85    U => "u",
86    V => "v",
87    W => "w",
88    X => "x",
89    Y => "y",
90    Z => "z",
91    Digit0 => "0",
92    Digit1 => "1",
93    Digit2 => "2",
94    Digit3 => "3",
95    Digit4 => "4",
96    Digit5 => "5",
97    Digit6 => "6",
98    Digit7 => "7",
99    Digit8 => "8",
100    Digit9 => "9",
101    Semicolon => ";",
102    Up => "Up",
103    Down => "Down",
104    Left => "Left",
105    Right => "Right",
106    Enter => "Enter",
107    Escape => "Escape",
108    Space => "Space",
109    Tab => "Tab",
110    Backspace => "Backspace",
111    F1 => "F1",
112    F2 => "F2",
113    F3 => "F3",
114    F4 => "F4",
115    F5 => "F5",
116    F6 => "F6",
117    F7 => "F7",
118    F8 => "F8",
119    F9 => "F9",
120    F10 => "F10",
121    F11 => "F11",
122    F12 => "F12",
123    Comma => ",",
124    Period => ".",
125    Slash => "/",
126    Apostrophe => "'",
127    LeftBracket => "[",
128    RightBracket => "]",
129    Backslash => "\\",
130    Minus => "-",
131    Equal => "=",
132    Backquote => "`",
133    Insert => "Insert",
134    Delete => "Delete",
135    Home => "Home",
136    End => "End",
137    PageUp => "PageUp",
138    PageDown => "PageDown",
139    Kp0 => "Kp0",
140    Kp1 => "Kp1",
141    Kp2 => "Kp2",
142    Kp3 => "Kp3",
143    Kp4 => "Kp4",
144    Kp5 => "Kp5",
145    Kp6 => "Kp6",
146    Kp7 => "Kp7",
147    Kp8 => "Kp8",
148    Kp9 => "Kp9",
149    KpAdd => "Kp+",
150    KpSubtract => "Kp-",
151    KpMultiply => "Kp*",
152    KpDivide => "Kp/",
153    KpDecimal => "Kp.",
154}
155
156impl Key {
157    /// Looks a key up by its [`name`](Self::name) (exact match).
158    pub fn from_name(name: &str) -> Option<Self> {
159        Self::ALL.iter().copied().find(|k| k.name() == name)
160    }
161}
162
163/// Key slots per action: a layout lists at most this many chords for an
164/// action, and the player can give each action up to this many keys
165/// (`docs/design/controls.md`, *Rebinding keys*).
166pub const SLOTS: usize = 3;
167
168impl fmt::Display for Key {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        f.write_str(self.name())
171    }
172}
173
174/// A key plus modifier state, e.g. `h` or `Shift+h`. `Shift+h` and `h` are
175/// different chords.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
177pub struct Chord {
178    /// The key pressed.
179    pub key: Key,
180    /// Whether Shift was held.
181    pub shift: bool,
182}
183
184/// Prefix for shifted chords in keymap files.
185const SHIFT_PREFIX: &str = "Shift+";
186
187impl Chord {
188    /// A chord without modifiers.
189    pub const fn plain(key: Key) -> Self {
190        Self { key, shift: false }
191    }
192
193    /// A chord with Shift held.
194    pub const fn shifted(key: Key) -> Self {
195        Self { key, shift: true }
196    }
197
198    /// Whether this chord is fixed by the game and can never be bound
199    /// (`docs/design/controls.md`, *Rebinding keys*): plain `Escape` always
200    /// cancels and plain `Delete` empties a key slot. `Shift+Escape` and
201    /// `Shift+Delete` are ordinary chords.
202    pub fn is_reserved(self) -> bool {
203        !self.shift && matches!(self.key, Key::Escape | Key::Delete)
204    }
205
206    /// Parses `"h"`, `"Shift+h"`, `"Left"`, `"Shift+Tab"`, `"F12"`, …
207    pub fn parse(s: &str) -> Result<Self, String> {
208        let (shift, name) = match s.strip_prefix(SHIFT_PREFIX) {
209            Some(rest) => (true, rest),
210            None => (false, s),
211        };
212        match Key::from_name(name) {
213            Some(key) => Ok(Self { key, shift }),
214            None => Err(unknown_key_message(s, name)),
215        }
216    }
217}
218
219fn unknown_key_message(chord: &str, name: &str) -> String {
220    let hint = if name.len() == 1 && name.bytes().all(|b| b.is_ascii_uppercase()) {
221        format!(
222            " (letters are lowercase; write \"{SHIFT_PREFIX}{}\" for a shifted letter)",
223            name.to_ascii_lowercase()
224        )
225    } else {
226        String::new()
227    };
228    format!(
229        "unknown key chord \"{chord}\"{hint}; expected a key such as \"h\", \"0\", \";\", \"Left\", \
230         \"Enter\", \"Escape\", \"Space\", \"Tab\", \"Backspace\" or \"F1\", optionally \
231         prefixed by \"{SHIFT_PREFIX}\""
232    )
233}
234
235impl FromStr for Chord {
236    type Err = String;
237
238    fn from_str(s: &str) -> Result<Self, Self::Err> {
239        Self::parse(s)
240    }
241}
242
243impl fmt::Display for Chord {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        if self.shift {
246            f.write_str(SHIFT_PREFIX)?;
247        }
248        f.write_str(self.key.name())
249    }
250}
251
252/// Declares [`Button`] with its docs, keeping the variant list,
253/// [`Button::ALL`] and [`Button::name`] in one table.
254macro_rules! buttons {
255    ($($variant:ident => $doc:literal,)+) => {
256        /// A controller button, named by its **position** on an Xbox-shaped
257        /// pad, whatever is printed on it (game ADR-0034). Each stick's four
258        /// directions count as buttons too, and so does a crank wound one
259        /// notch either way (the Playdate's, ticket 1035: pressed and let go
260        /// once per notch), and so does an item of the game's own in the
261        /// system's menu (pressed and let go when the player picks it,
262        /// ADR-0008 §3). `app` translates what the platform reports to
263        /// these; anything else on a pad is ignored, and so are the
264        /// system's own buttons (the Playdate's Menu and Lock, the Deck's
265        /// Steam and Quick Access), which no keymap can name.
266        ///
267        /// The first 16 are in the order of the browser Gamepad API's
268        /// "standard" mapping.
269        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
270        pub enum Button {
271            $(
272                #[doc = $doc]
273                $variant,
274            )+
275        }
276
277        impl Button {
278            /// Every button, in declaration order.
279            pub const ALL: &'static [Button] = &[$(Button::$variant),+];
280
281            /// The name used in keymap files (the variant name).
282            pub fn name(self) -> &'static str {
283                match self {
284                    $(Button::$variant => stringify!($variant),)+
285                }
286            }
287        }
288    };
289}
290
291buttons! {
292    South => "The bottom face button (Xbox `A`).",
293    East => "The right face button (Xbox `B`).",
294    West => "The left face button (Xbox `X`).",
295    North => "The top face button (Xbox `Y`).",
296    LeftShoulder => "The left shoulder button (Xbox `LB`).",
297    RightShoulder => "The right shoulder button (Xbox `RB`).",
298    LeftTrigger => "The left trigger (Xbox `LT`), pulled far enough to count as pressed.",
299    RightTrigger => "The right trigger (Xbox `RT`), pulled far enough to count as pressed.",
300    Select => "The left centre button (Xbox `Back`).",
301    Start => "The right centre button (Xbox `Start`).",
302    LeftStickPress => "Pressing the left stick in.",
303    RightStickPress => "Pressing the right stick in.",
304    DpadUp => "D-pad up.",
305    DpadDown => "D-pad down.",
306    DpadLeft => "D-pad left.",
307    DpadRight => "D-pad right.",
308    LeftStickUp => "The left stick pushed up.",
309    LeftStickDown => "The left stick pushed down.",
310    LeftStickLeft => "The left stick pushed left.",
311    LeftStickRight => "The left stick pushed right.",
312    RightStickUp => "The right stick pushed up.",
313    RightStickDown => "The right stick pushed down.",
314    RightStickLeft => "The right stick pushed left.",
315    RightStickRight => "The right stick pushed right.",
316    CrankBack => "The crank wound one notch backwards (counterclockwise).",
317    CrankForward => "The crank wound one notch forwards (clockwise).",
318    SystemMenu0 => "The first item of the game's own in the system's menu.",
319    SystemMenu1 => "The second item of the game's own in the system's menu.",
320    SystemMenu2 => "The third item of the game's own in the system's menu.",
321    SystemMenu3 => "The fourth item of the game's own in the system's menu.",
322    SystemMenu4 => "The fifth item of the game's own in the system's menu.",
323    SystemMenu5 => "The sixth item of the game's own in the system's menu.",
324    SystemMenu6 => "The seventh item of the game's own in the system's menu.",
325    SystemMenu7 => "The eighth item of the game's own in the system's menu.",
326}
327
328/// The system menu items a keymap can name: [`Button::SystemMenu0`] to
329/// [`Button::SystemMenu7`]. A target holds fewer
330/// (`shuyi_assets::target::Input::system_menu_slots`).
331pub const SYSTEM_MENU_ITEMS: u8 = 8;
332
333impl Button {
334    /// The system menu's item `slot` (from 0), if a keymap can name it
335    /// (below [`SYSTEM_MENU_ITEMS`]).
336    pub fn system_menu(slot: u8) -> Option<Self> {
337        let first = Self::ALL.iter().position(|&b| b == Self::SystemMenu0)?;
338        Self::ALL.get(first + usize::from(slot)).copied()
339    }
340
341    /// Which item of the system's menu this is (from 0), if it is one.
342    pub fn system_menu_slot(self) -> Option<u8> {
343        (0..SYSTEM_MENU_ITEMS).find(|&slot| Self::system_menu(slot) == Some(self))
344    }
345
346    /// Looks a button up by its [`name`](Self::name) (exact match).
347    pub fn from_name(name: &str) -> Option<Self> {
348        Self::ALL.iter().copied().find(|b| b.name() == name)
349    }
350
351    /// Parses a keymap-file button name: `"South"`, `"DpadUp"`,
352    /// `"LeftStickLeft"`, …
353    pub fn parse(s: &str) -> Result<Self, String> {
354        Self::from_name(s).ok_or_else(|| {
355            let known: Vec<&str> = Self::ALL.iter().map(|b| b.name()).collect();
356            format!(
357                "unknown button \"{s}\"; known buttons: {}",
358                known.join(", ")
359            )
360        })
361    }
362}
363
364impl FromStr for Button {
365    type Err = String;
366
367    fn from_str(s: &str) -> Result<Self, Self::Err> {
368        Self::parse(s)
369    }
370}
371
372/// A button in a data file (a target's inputs, `shuyi_assets::target`) and
373/// in a baked keymap (`pack`) is its [`name`](Button::name).
374impl Serialize for Button {
375    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
376        serializer.serialize_str(self.name())
377    }
378}
379
380impl<'de> Deserialize<'de> for Button {
381    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
382        let name = String::deserialize(deserializer)?;
383        Self::parse(&name).map_err(serde::de::Error::custom)
384    }
385}
386
387impl fmt::Display for Button {
388    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389        f.write_str(self.name())
390    }
391}
392
393/// What a controller binding is: one [`Button`], or two pressed together
394/// (a *button chord*, ticket 1034), written `"South+East"` in keymap files
395/// and saved bindings. The two are kept in [`Button::ALL`] order, so
396/// `"East+South"` is the same chord as `"South+East"` (and is shown as the
397/// latter). How a chord fires is the input's business
398/// (`shuyi_ui::input::state`).
399#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
400pub struct ButtonChord {
401    first: Button,
402    second: Option<Button>,
403}
404
405/// What joins a chord's two buttons in keymap files and saved bindings.
406const CHORD_JOIN: char = '+';
407
408impl ButtonChord {
409    /// One button on its own.
410    pub const fn single(button: Button) -> Self {
411        Self {
412            first: button,
413            second: None,
414        }
415    }
416
417    /// `a` and `b` pressed together, in either order; `None` if they are
418    /// the same button.
419    pub fn pair(a: Button, b: Button) -> Option<Self> {
420        let (first, second) = match a.cmp(&b) {
421            core::cmp::Ordering::Less => (a, b),
422            core::cmp::Ordering::Greater => (b, a),
423            core::cmp::Ordering::Equal => return None,
424        };
425        Some(Self {
426            first,
427            second: Some(second),
428        })
429    }
430
431    /// The button, or a chord's first button in [`Button::ALL`] order.
432    pub fn first(self) -> Button {
433        self.first
434    }
435
436    /// The button, if this is one button on its own.
437    pub fn single_button(self) -> Option<Button> {
438        match self.second {
439            None => Some(self.first),
440            Some(_) => None,
441        }
442    }
443
444    /// The two buttons, in [`Button::ALL`] order, if this is a chord.
445    pub fn buttons(self) -> Option<(Button, Button)> {
446        self.second.map(|second| (self.first, second))
447    }
448
449    /// The other button of a chord that has `button`, if this is one.
450    pub fn partner(self, button: Button) -> Option<Button> {
451        let (a, b) = self.buttons()?;
452        if a == button {
453            Some(b)
454        } else if b == button {
455            Some(a)
456        } else {
457            None
458        }
459    }
460
461    /// Parses `"South"` or `"South+East"` (either order).
462    pub fn parse(s: &str) -> Result<Self, String> {
463        let Some((a, b)) = s.split_once(CHORD_JOIN) else {
464            return Button::parse(s).map(Self::single);
465        };
466        let (a, b) = (Button::parse(a.trim())?, Button::parse(b.trim())?);
467        Self::pair(a, b).ok_or_else(|| format!("\"{s}\" names the same button twice"))
468    }
469}
470
471impl From<Button> for ButtonChord {
472    fn from(button: Button) -> Self {
473        Self::single(button)
474    }
475}
476
477/// A binding is a button when it is that button on its own.
478impl PartialEq<Button> for ButtonChord {
479    fn eq(&self, button: &Button) -> bool {
480        self.single_button() == Some(*button)
481    }
482}
483
484impl FromStr for ButtonChord {
485    type Err = String;
486
487    fn from_str(s: &str) -> Result<Self, Self::Err> {
488        Self::parse(s)
489    }
490}
491
492impl fmt::Display for ButtonChord {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        f.write_str(self.first.name())?;
495        if let Some(second) = self.second {
496            write!(f, "{CHORD_JOIN}{}", second.name())?;
497        }
498        Ok(())
499    }
500}
501
502/// Key-repeat timings for held repeatable actions.
503#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
504#[serde(deny_unknown_fields)]
505pub struct RepeatDef {
506    /// Time from the press to the first repeat, in milliseconds.
507    pub delay_ms: u32,
508    /// Time between later repeats, in milliseconds (never 0).
509    pub interval_ms: u32,
510}
511
512impl Default for RepeatDef {
513    fn default() -> Self {
514        Self {
515            delay_ms: 300,
516            interval_ms: 55,
517        }
518    }
519}
520
521/// How far a stick must be pushed to count as a held direction
522/// ([`Button::LeftStickUp`] and the rest), in percent of its full travel.
523/// `release_percent` is below `press_percent`, so a stick resting near the
524/// edge of the dead zone doesn't flicker.
525#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
526#[serde(deny_unknown_fields)]
527pub struct StickDef {
528    /// A direction starts once the stick is pushed at least this far.
529    pub press_percent: u8,
530    /// A held direction ends once the stick falls below this.
531    pub release_percent: u8,
532}
533
534impl Default for StickDef {
535    fn default() -> Self {
536        Self {
537            press_percent: 50,
538            release_percent: 35,
539        }
540    }
541}
542
543/// How [`ButtonChord`]s fire (`shuyi_ui::input::state`): a chord button
544/// whose own action waits for its partner waits at most `window_ms`
545/// (unless it waits for its release).
546#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
547#[serde(deny_unknown_fields)]
548pub struct ChordDef {
549    /// How long, in milliseconds, two buttons pressed one after the other
550    /// still count as pressed together.
551    pub window_ms: u32,
552}
553
554impl Default for ChordDef {
555    fn default() -> Self {
556        Self { window_ms: 80 }
557    }
558}
559
560/// How a crank turns into [`Button::CrankBack`] and
561/// [`Button::CrankForward`] presses (`shuyi_ui::input::crank`): one per
562/// `notch_degrees` wound.
563#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
564#[serde(deny_unknown_fields)]
565pub struct CrankDef {
566    /// How far, in degrees, the crank is wound for one notch (1 to 360).
567    pub notch_degrees: u16,
568}
569
570impl Default for CrankDef {
571    fn default() -> Self {
572        Self { notch_degrees: 45 }
573    }
574}
575
576/// Most bytes in a [`Layout`]'s name.
577pub const LAYOUT_NAME_MAX: usize = 24;
578
579/// A complete set of key bindings the player can pick: one entry of
580/// `layouts` in the keymap file, named by its `id`. The file decides which
581/// layouts there are; this is only the name, kept inline so a layout is
582/// `Copy`. Names are ASCII letters, digits and `_`, at most
583/// [`LAYOUT_NAME_MAX`] bytes (`RightHanded`).
584#[derive(Clone, Copy, PartialEq, Eq, Hash)]
585pub struct Layout {
586    len: usize,
587    bytes: [u8; LAYOUT_NAME_MAX],
588}
589
590impl Layout {
591    /// The layout named `name`, for a game's constants. Fails to compile in
592    /// a `const` (panics elsewhere) if `name` isn't a valid name.
593    pub const fn named(name: &'static str) -> Self {
594        match Self::try_named(name) {
595            Some(layout) => layout,
596            None => panic!("a layout name is 1 to 24 ASCII letters, digits or _"),
597        }
598    }
599
600    /// The layout named `name`, if it is a valid name (saved settings, the
601    /// keymap file). Whether a keymap has it is
602    /// [`KeymapDef::has_layout`]'s question.
603    pub fn from_name(name: &str) -> Option<Self> {
604        Self::try_named(name)
605    }
606
607    const fn try_named(name: &str) -> Option<Self> {
608        let given = name.as_bytes();
609        if given.is_empty() || given.len() > LAYOUT_NAME_MAX {
610            return None;
611        }
612        let mut bytes = [0; LAYOUT_NAME_MAX];
613        let mut i = 0;
614        while i < given.len() {
615            let b = given[i];
616            if !(b.is_ascii_alphanumeric() || b == b'_') {
617                return None;
618            }
619            bytes[i] = b;
620            i += 1;
621        }
622        Some(Self {
623            len: given.len(),
624            bytes,
625        })
626    }
627
628    /// The name used in keymap files and saved settings.
629    pub fn name(&self) -> &str {
630        let bytes = self.bytes.get(..self.len).unwrap_or_default();
631        // Only ASCII is ever stored.
632        core::str::from_utf8(bytes).unwrap_or_default()
633    }
634}
635
636impl Ord for Layout {
637    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
638        self.name().cmp(other.name())
639    }
640}
641
642impl PartialOrd for Layout {
643    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
644        Some(self.cmp(other))
645    }
646}
647
648impl fmt::Debug for Layout {
649    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650        write!(f, "Layout({})", self.name())
651    }
652}
653
654impl fmt::Display for Layout {
655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656        f.write_str(self.name())
657    }
658}
659
660/// A layout's binary form (a baked keymap, `pack`) is its name.
661impl Serialize for Layout {
662    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
663        serializer.serialize_str(self.name())
664    }
665}
666
667impl<'de> Deserialize<'de> for Layout {
668    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
669        let name = String::deserialize(deserializer)?;
670        Self::from_name(&name)
671            .ok_or_else(|| serde::de::Error::custom(format!("{name:?} is not a layout name")))
672    }
673}
674
675/// Validated bindings as a lookup: each chord maps to exactly one action
676/// (of one [group](ActionSet::Group)). Unbound chords are absent.
677pub type Bindings<A> = BTreeMap<Chord, A>;
678
679/// One layout's default keys: every action with its chords in the order
680/// the keymap file lists them (at most [`SLOTS`], none [reserved]), no chord
681/// on two actions of one [group](ActionSet::Group). The order matters: the
682/// first chord is the one help text names, and the chords fill the player's
683/// key slots in this order.
684///
685/// [reserved]: Chord::is_reserved
686pub type LayoutKeys<A> = BTreeMap<A, Vec<Chord>>;
687
688/// The default controller buttons: every action with its buttons and
689/// [button chords](ButtonChord) in the order the keymap file lists them (at
690/// most [`SLOTS`]), none on two actions of one [group](ActionSet::Group).
691/// One table for every layout.
692pub type PadKeys<A> = BTreeMap<A, Vec<ButtonChord>>;
693
694/// The validated keymap: every [`Layout`]'s default keys, the layout
695/// picker's bindings, the default controller buttons, plus the key-repeat
696/// timings and stick thresholds they share.
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698pub struct KeymapDef<A: ActionSet> {
699    /// Layout → its default keys. After
700    /// [`from_source`](Self::from_source) every layout the file lists is
701    /// present, with every action.
702    pub layouts: BTreeMap<Layout, LayoutKeys<A>>,
703    /// The layouts in the order the file lists them (the order the layout
704    /// picker shows them), each with the text key of its name.
705    pub layout_names: LayoutNames,
706    /// The keys that work before any layout is chosen (the layout picker).
707    /// Actions not listed are unbound there.
708    pub layout_picker: Bindings<A>,
709    /// The default controller buttons, the same in every layout and the
710    /// layout picker. After [`from_source`](Self::from_source) every action
711    /// is present.
712    pub pad: PadKeys<A>,
713    /// The default buttons of a Playdate (game ADR-0070, ticket 1034), whose
714    /// only buttons are the D-pad, A and B: the file's `playdate` table, or
715    /// a copy of [`pad`](Self::pad) for a file without one. A game running
716    /// on a Playdate uses it in place of `pad`
717    /// ([`for_playdate`](Self::for_playdate)). The same as
718    /// `targets["playdate"]` when the file has one.
719    pub playdate: PadKeys<A>,
720    /// The default buttons on a target, by its profile's id
721    /// (`shuyi_assets::target`, ADR-0008 §3): the file's `targets` table,
722    /// with its `playdate` table as `"playdate"`. A target in it plays with
723    /// its own table in place of [`pad`](Self::pad)
724    /// ([`for_target`](Self::for_target)), and `shuyi check --target`
725    /// checks the target's controls on it.
726    pub targets: BTreeMap<String, PadKeys<A>>,
727    /// Key-repeat timings, for held keys and held buttons alike.
728    pub repeat: RepeatDef,
729    /// How button chords fire (optional in the file).
730    pub chord: ChordDef,
731    /// How far a stick is pushed to count as a direction.
732    pub stick: StickDef,
733    /// How far a crank is wound for one notch (optional in the file).
734    pub crank: CrankDef,
735}
736
737impl<A: ActionSet> Default for KeymapDef<A> {
738    fn default() -> Self {
739        Self {
740            layouts: BTreeMap::new(),
741            layout_names: Vec::new(),
742            layout_picker: BTreeMap::new(),
743            pad: BTreeMap::new(),
744            playdate: BTreeMap::new(),
745            targets: BTreeMap::new(),
746            repeat: RepeatDef::default(),
747            chord: ChordDef::default(),
748            stick: StickDef::default(),
749            crank: CrankDef::default(),
750        }
751    }
752}
753
754impl<A: ActionSet> AssetKind for KeymapDef<A> {
755    const NAME: &'static str = "keymap";
756}
757
758/// The keymap file (`keymap`, `.ron`) of a game whose actions are `A`,
759/// checked as [`KeymapDef::from_source`] says.
760pub struct KeymapRon<A>(PhantomData<fn() -> A>);
761
762impl<A> KeymapRon<A> {
763    /// The format, for a game's actions `A`.
764    pub const fn new() -> Self {
765        Self(PhantomData)
766    }
767}
768
769impl<A> Default for KeymapRon<A> {
770    fn default() -> Self {
771        Self::new()
772    }
773}
774
775impl<A> fmt::Debug for KeymapRon<A> {
776    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
777        f.write_str("KeymapRon")
778    }
779}
780
781impl<A: ActionSet> Format for KeymapRon<A> {
782    type Asset = KeymapDef<A>;
783
784    fn id(&self) -> &'static str {
785        "keymap"
786    }
787
788    fn extensions(&self) -> &'static [&'static str] {
789        &["ron"]
790    }
791
792    fn max_bytes(&self) -> usize {
793        TEXT_MAX_BYTES
794    }
795
796    fn decode(
797        &self,
798        file: &AssetPath,
799        bytes: &[u8],
800        _cx: &DecodeCx<'_>,
801    ) -> Result<KeymapDef<A>, Vec<ContentError>> {
802        KeymapDef::from_source(file.shown(), text(file, bytes)?)
803    }
804}
805
806/// The layouts in file order, each with the text key of its name.
807pub type LayoutNames = Vec<(Layout, String)>;
808
809/// The file as written, before validation.
810#[derive(Deserialize)]
811#[serde(deny_unknown_fields)]
812struct RawKeymap {
813    layouts: Vec<RawLayout>,
814    layout_picker: BTreeMap<String, Vec<String>>,
815    pad: BTreeMap<String, Vec<String>>,
816    /// Absent (empty) in a file for no Playdate.
817    #[serde(default)]
818    playdate: BTreeMap<String, Vec<String>>,
819    /// Target id → its default buttons; absent (empty) in a file with
820    /// none.
821    #[serde(default)]
822    targets: BTreeMap<String, BTreeMap<String, Vec<String>>>,
823    repeat: RepeatDef,
824    #[serde(default)]
825    chord: ChordDef,
826    stick: StickDef,
827    #[serde(default)]
828    crank: CrankDef,
829}
830
831/// One entry of `layouts`, before validation.
832#[derive(Deserialize)]
833#[serde(deny_unknown_fields)]
834struct RawLayout {
835    /// The name saved settings know it by.
836    id: String,
837    /// The text key of the name the player sees.
838    name: String,
839    /// Action name → chord names.
840    keys: BTreeMap<String, Vec<String>>,
841}
842
843impl<A: ActionSet> KeymapDef<A> {
844    /// `layout`'s default bindings of the actions in `group`, as a chord
845    /// lookup (`None` if the keymap has no such layout).
846    pub fn bindings(&self, layout: Layout, group: A::Group) -> Option<Bindings<A>> {
847        let keys = self.layouts.get(&layout)?;
848        Some(
849            keys.iter()
850                .filter(|(a, _)| a.group() == group)
851                .flat_map(|(&a, chords)| chords.iter().map(move |&c| (c, a)))
852                .collect(),
853        )
854    }
855
856    /// `action`'s default chords in `layout`, in file order; empty if the
857    /// layout or action is missing.
858    pub fn chords(&self, layout: Layout, action: A) -> &[Chord] {
859        self.layouts
860            .get(&layout)
861            .and_then(|keys| keys.get(&action))
862            .map_or(&[], Vec::as_slice)
863    }
864
865    /// `action`'s default controller buttons and button chords, in file
866    /// order; empty if the action is missing (only in a definition built by
867    /// hand).
868    pub fn buttons(&self, action: A) -> &[ButtonChord] {
869        self.pad.get(&action).map_or(&[], Vec::as_slice)
870    }
871
872    /// This keymap as a Playdate uses it: its [`playdate`](Self::playdate)
873    /// table as the default buttons.
874    #[must_use]
875    pub fn for_playdate(mut self) -> Self {
876        self.pad.clone_from(&self.playdate);
877        self
878    }
879
880    /// The default buttons of target `id` (its profile's id), if the file
881    /// gives that target a table of its own ([`targets`](Self::targets)).
882    pub fn target_buttons(&self, id: &str) -> Option<&PadKeys<A>> {
883        self.targets.get(id)
884    }
885
886    /// This keymap as target `id` uses it: the target's own table as the
887    /// default buttons, if it has one, else [`pad`](Self::pad).
888    #[must_use]
889    pub fn for_target(mut self, id: &str) -> Self {
890        if let Some(buttons) = self.targets.get(id) {
891            self.pad = buttons.clone();
892        }
893        self
894    }
895
896    /// Whether the keymap has `layout` (a name from saved settings may
897    /// name one it no longer has).
898    pub fn has_layout(&self, layout: Layout) -> bool {
899        self.layouts.contains_key(&layout)
900    }
901
902    /// The layouts in file order: the order the layout picker lists them.
903    pub fn layout_ids(&self) -> impl Iterator<Item = Layout> + '_ {
904        self.layout_names.iter().map(|&(layout, _)| layout)
905    }
906
907    /// The first layout the file lists: the one a screen falls back on
908    /// while none is chosen.
909    pub fn first_layout(&self) -> Option<Layout> {
910        self.layout_ids().next()
911    }
912
913    /// The text key of `layout`'s name, if the keymap has it.
914    pub fn layout_text(&self, layout: Layout) -> Option<&str> {
915        let (_, text) = self.layout_names.iter().find(|&&(l, _)| l == layout)?;
916        Some(text)
917    }
918
919    /// Parses and validates keymap `source`, attributing errors to `file`.
920    /// Reports a file with no layouts, a bad or repeated layout `id`, and
921    /// within each layout every unknown action, unparsable chord, chord
922    /// bound more than once, [reserved](Chord::is_reserved) chord, action
923    /// with more than [`SLOTS`] chords and missing action (prefixed with the
924    /// layout's id), the same for the `layout_picker` section (except that
925    /// unlisted actions are unbound there, and an action may have more than
926    /// [`SLOTS`] chords: the picker isn't rebindable), and for the `pad`
927    /// and `playdate` sections and each table of `targets` (buttons and
928    /// [button chords](ButtonChord) instead of key chords, every action
929    /// listed, none on an action that [can't be
930    /// rebound](ActionSet::is_rebindable), no system menu item in a chord),
931    /// plus a bad target id, a Playdate table given both ways, bad repeat
932    /// timing, stick thresholds and crank notch size.
933    pub fn from_source(file: &str, source: &str) -> Result<Self, Vec<ContentError>> {
934        let raw: RawKeymap = parse_ron(file, source).map_err(|e| vec![e])?;
935        let mut errors = Vec::new();
936        let (layouts, layout_names) =
937            validate_layouts::<A>(file, source, &raw.layouts, &mut errors);
938        let picker_line = line_of_quoted(source, 0, "layout_picker");
939        let from = picker_line.map_or(0, |line| usize::try_from(line).unwrap_or(0));
940        let layout_picker = match validate_bindings::<A, Chord>(
941            file,
942            source,
943            from,
944            "layout_picker",
945            false,
946            &raw.layout_picker,
947        ) {
948            Ok(keys) => keys
949                .iter()
950                .flat_map(|(&a, chords)| chords.iter().map(move |&c| (c, a)))
951                .collect(),
952            Err(e) => {
953                errors.extend(e);
954                Bindings::new()
955            }
956        };
957        let pad = validate_pad::<A>(file, source, "pad", "pad", &raw.pad, &mut errors);
958        let mut targets = validate_targets::<A>(file, source, &raw.targets, &mut errors);
959        let playdate = if raw.playdate.is_empty() {
960            targets
961                .get(PLAYDATE)
962                .cloned()
963                .unwrap_or_else(|| pad.clone())
964        } else {
965            if raw.targets.contains_key(PLAYDATE) {
966                errors.push(positioned(
967                    ContentError::new(
968                        file,
969                        "the Playdate's buttons are given twice: as `playdate` and as \
970                         `targets.\"playdate\"` (keep one)",
971                    ),
972                    line_of_quoted(source, 0, PLAYDATE),
973                ));
974            }
975            let table =
976                validate_pad::<A>(file, source, PLAYDATE, PLAYDATE, &raw.playdate, &mut errors);
977            targets.insert(PLAYDATE.to_owned(), table.clone());
978            table
979        };
980        if raw.repeat.interval_ms == 0 {
981            errors.push(positioned(
982                ContentError::new(file, "repeat interval_ms must be at least 1")
983                    .with_code(codes::OUT_OF_RANGE),
984                line_of_quoted(source, 0, "repeat"),
985            ));
986        }
987        let StickDef {
988            press_percent,
989            release_percent,
990        } = raw.stick;
991        if release_percent == 0 || release_percent >= press_percent || press_percent > 100 {
992            errors.push(positioned(
993                ContentError::new(
994                    file,
995                    "stick release_percent must be at least 1 and below press_percent, which \
996                     must be at most 100",
997                ),
998                line_of_quoted(source, 0, "stick"),
999            ));
1000        }
1001        if !(1..=360).contains(&raw.crank.notch_degrees) {
1002            errors.push(positioned(
1003                ContentError::new(file, "crank notch_degrees must be 1 to 360")
1004                    .with_code(codes::OUT_OF_RANGE),
1005                line_of_quoted(source, 0, "crank"),
1006            ));
1007        }
1008        if errors.is_empty() {
1009            Ok(Self {
1010                layouts,
1011                layout_names,
1012                layout_picker,
1013                pad,
1014                playdate,
1015                targets,
1016                repeat: raw.repeat,
1017                chord: raw.chord,
1018                stick: raw.stick,
1019                crank: raw.crank,
1020            })
1021        } else {
1022            Err(errors)
1023        }
1024    }
1025}
1026
1027/// The layouts the file lists (`layouts`): each one's validated keys, and
1028/// the layouts in file order with the text keys of their names. Errors go
1029/// to `errors`: no layout at all, a bad or repeated id, and every error of
1030/// a layout's keys.
1031fn validate_layouts<A: ActionSet>(
1032    file: &str,
1033    source: &str,
1034    raw: &[RawLayout],
1035    errors: &mut Vec<ContentError>,
1036) -> (BTreeMap<Layout, LayoutKeys<A>>, LayoutNames) {
1037    let mut layouts = BTreeMap::new();
1038    let mut layout_names = Vec::new();
1039    if raw.is_empty() {
1040        errors.push(positioned(
1041            ContentError::new(file, "layouts: list at least one layout"),
1042            line_of_quoted(source, 0, "layouts"),
1043        ));
1044    }
1045    let mut listed = BTreeSet::new();
1046    let mut search_from = 0;
1047    for RawLayout { id, name, keys } in raw {
1048        let start = line_of_quoted(source, search_from, id);
1049        // Search the layout's own lines first, so errors in the second
1050        // layout don't point into the first.
1051        let from = start.map_or(search_from, |line| {
1052            usize::try_from(line).unwrap_or(search_from)
1053        });
1054        search_from = from;
1055        let Some(layout) = Layout::from_name(id) else {
1056            let message = format!(
1057                "layout id \"{id}\" must be 1 to {LAYOUT_NAME_MAX} ASCII letters, digits or _"
1058            );
1059            errors.push(positioned(ContentError::new(file, message), start));
1060            continue;
1061        };
1062        if !listed.insert(layout) {
1063            let message = format!("layout \"{layout}\" is listed twice");
1064            errors.push(positioned(
1065                ContentError::new(file, message).with_code(codes::DUPLICATE),
1066                start,
1067            ));
1068            continue;
1069        }
1070        let label = format!("layout \"{layout}\"");
1071        match validate_bindings::<A, Chord>(file, source, from, &label, true, keys) {
1072            Ok(keys) => {
1073                layouts.insert(layout, keys);
1074                layout_names.push((layout, name.clone()));
1075            }
1076            Err(e) => errors.extend(e),
1077        }
1078    }
1079    (layouts, layout_names)
1080}
1081
1082/// The Playdate's target id, and the name of its table of the file.
1083const PLAYDATE: &str = "playdate";
1084
1085/// Most bytes in a target id of the `targets` table.
1086const TARGET_ID_MAX: usize = 32;
1087
1088/// Whether `id` can be a target's id: 1 to [`TARGET_ID_MAX`] lowercase
1089/// ASCII letters, digits and `-`, as the profiles' files are named
1090/// (`steam-deck`).
1091fn is_target_id(id: &str) -> bool {
1092    (1..=TARGET_ID_MAX).contains(&id.len())
1093        && id
1094            .bytes()
1095            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
1096}
1097
1098/// The file's `targets`: each target's table of default buttons,
1099/// validated as `pad` is, by target id. Errors go to `errors`: a bad id,
1100/// and every error of a table.
1101fn validate_targets<A: ActionSet>(
1102    file: &str,
1103    source: &str,
1104    raw: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
1105    errors: &mut Vec<ContentError>,
1106) -> BTreeMap<String, PadKeys<A>> {
1107    let mut targets = BTreeMap::new();
1108    for (id, table) in raw {
1109        if !is_target_id(id) {
1110            let message = format!(
1111                "targets: target id \"{id}\" must be 1 to {TARGET_ID_MAX} lowercase ASCII \
1112                 letters, digits or -"
1113            );
1114            errors.push(positioned(
1115                ContentError::new(file, message),
1116                line_of_quoted(source, 0, id),
1117            ));
1118            continue;
1119        }
1120        let label = format!("targets.\"{id}\"");
1121        let before = errors.len();
1122        let buttons = validate_pad::<A>(file, source, &label, id, table, errors);
1123        if errors.len() == before {
1124            targets.insert(id.clone(), buttons);
1125        }
1126    }
1127    targets
1128}
1129
1130/// A table of default controller buttons (`pad`, `playdate`, a target's)
1131/// named `label` in messages, validated; its errors go to `errors`. Its
1132/// lines are searched for from the first that has `anchor` quoted or as a
1133/// field.
1134fn validate_pad<A: ActionSet>(
1135    file: &str,
1136    source: &str,
1137    label: &str,
1138    anchor: &str,
1139    raw: &BTreeMap<String, Vec<String>>,
1140    errors: &mut Vec<ContentError>,
1141) -> PadKeys<A> {
1142    let line = line_of_quoted(source, 0, anchor);
1143    let from = line.map_or(0, |line| usize::try_from(line).unwrap_or(0));
1144    let pad = match validate_bindings::<A, ButtonChord>(file, source, from, label, true, raw) {
1145        Ok(buttons) => buttons,
1146        Err(e) => {
1147            errors.extend(e);
1148            return PadKeys::new();
1149        }
1150    };
1151    for (&action, buttons) in &pad {
1152        if !action.is_rebindable() && !buttons.is_empty() {
1153            let message = format!(
1154                "{label}: action \"{}\" can't have a button: it can't be rebound, so it is \
1155                 keyboard only",
1156                action.name()
1157            );
1158            errors.push(positioned(
1159                ContentError::new(file, message),
1160                line_of_quoted(source, from, action.name()),
1161            ));
1162        }
1163    }
1164    pad
1165}
1166
1167/// Something `keymap.ron` binds to actions: a key [`Chord`] or a controller
1168/// [`ButtonChord`].
1169trait Bound: Copy + Ord + fmt::Display {
1170    /// What one is called in error messages.
1171    const NOUN: &'static str;
1172    /// What several are called in error messages.
1173    const PLURAL: &'static str;
1174
1175    /// Parses its keymap-file name.
1176    fn parse_name(text: &str) -> Result<Self, String>;
1177
1178    /// Why it may never appear in the file, if so.
1179    fn refused(self) -> Option<&'static str>;
1180}
1181
1182impl Bound for Chord {
1183    const NOUN: &'static str = "chord";
1184    const PLURAL: &'static str = "keys";
1185
1186    fn parse_name(text: &str) -> Result<Self, String> {
1187        Self::parse(text)
1188    }
1189
1190    fn refused(self) -> Option<&'static str> {
1191        self.is_reserved()
1192            .then_some("Esc and Delete are fixed, see controls.md")
1193    }
1194}
1195
1196impl Bound for ButtonChord {
1197    const NOUN: &'static str = "button";
1198    const PLURAL: &'static str = "buttons";
1199
1200    fn parse_name(text: &str) -> Result<Self, String> {
1201        Self::parse(text)
1202    }
1203
1204    fn refused(self) -> Option<&'static str> {
1205        self.buttons()
1206            .is_some_and(|(a, b)| a.system_menu_slot().or(b.system_menu_slot()).is_some())
1207            .then_some("a system menu item is picked on its own, never with another button")
1208    }
1209}
1210
1211/// Validates one section's `actions` (action name → chord or button
1212/// names): a layout, the layout picker or the `pad` table. Messages start
1213/// with `label`; lines are searched from line index `from` (just after the
1214/// section's own line) on. For a `complete` section (a layout, `pad`), every
1215/// action must be listed, with at most [`SLOTS`] entries. A chord or
1216/// button may be on one action of each [group](ActionSet::Group).
1217fn validate_bindings<A: ActionSet, T: Bound>(
1218    file: &str,
1219    source: &str,
1220    from: usize,
1221    label: &str,
1222    complete: bool,
1223    actions: &BTreeMap<String, Vec<String>>,
1224) -> Result<BTreeMap<A, Vec<T>>, Vec<ContentError>> {
1225    let noun = T::NOUN;
1226    let mut errors = Vec::new();
1227    let mut bindings: BTreeMap<(A::Group, T), A> = BTreeMap::new();
1228    let mut keys: BTreeMap<A, Vec<T>> = BTreeMap::new();
1229    let err_at = |key: &str, message: String| {
1230        positioned(
1231            ContentError::new(file, format!("{label}: {message}")),
1232            line_of_quoted(source, from, key),
1233        )
1234    };
1235    let coded_at = |key: &str, message: String, code| err_at(key, message).with_code(code);
1236    for (action_name, names) in actions {
1237        let Some(action) = A::from_name(action_name) else {
1238            let known: Vec<&str> = A::ALL.iter().map(|a| a.name()).collect();
1239            errors.push(coded_at(
1240                action_name,
1241                format!(
1242                    "unknown action \"{action_name}\"; known actions: {}",
1243                    known.join(", ")
1244                ),
1245                codes::UNKNOWN_NAME,
1246            ));
1247            continue;
1248        };
1249        if complete && names.len() > SLOTS {
1250            errors.push(err_at(
1251                action_name,
1252                format!(
1253                    "action \"{}\" has {} {}; at most {SLOTS} are allowed",
1254                    action.name(),
1255                    names.len(),
1256                    T::PLURAL
1257                ),
1258            ));
1259        }
1260        let name = action.name();
1261        let listed = keys.entry(action).or_default();
1262        for text in names {
1263            let bound = match T::parse_name(text) {
1264                Ok(bound) => bound,
1265                Err(why) => {
1266                    errors.push(coded_at(
1267                        action_name,
1268                        format!("action \"{action_name}\": {why}"),
1269                        codes::UNKNOWN_NAME,
1270                    ));
1271                    continue;
1272                }
1273            };
1274            if let Some(why) = bound.refused() {
1275                errors.push(err_at(
1276                    action_name,
1277                    format!("action \"{name}\": \"{bound}\" can't be bound here: {why}"),
1278                ));
1279            } else if let Some(&other) = bindings.get(&(action.group(), bound)) {
1280                let message = if other == action {
1281                    format!("{noun} \"{bound}\" is listed twice for \"{name}\"")
1282                } else {
1283                    format!(
1284                        "{noun} \"{bound}\" is bound to both \"{}\" and \"{name}\"",
1285                        other.name()
1286                    )
1287                };
1288                errors.push(coded_at(action_name, message, codes::DUPLICATE));
1289            } else {
1290                bindings.insert((action.group(), bound), action);
1291                listed.push(bound);
1292            }
1293        }
1294    }
1295    for action in A::ALL {
1296        if complete && !actions.contains_key(action.name()) {
1297            errors.push(ContentError::new(
1298                file,
1299                format!(
1300                    "{label}: missing action \"{}\" (list it with [] to leave it \
1301                     unbound)",
1302                    action.name()
1303                ),
1304            ));
1305        }
1306    }
1307    if errors.is_empty() {
1308        Ok(keys)
1309    } else {
1310        Err(errors)
1311    }
1312}
1313
1314/// `error` at `line`, if known.
1315fn positioned(error: ContentError, line: Option<u32>) -> ContentError {
1316    match line {
1317        Some(line) => error.at(line, None),
1318        None => error,
1319    }
1320}
1321
1322/// 1-based number of the first line, at or after line index `from`
1323/// (0-based), that contains `key` quoted or starts with the field `key:`,
1324/// for error positions.
1325fn line_of_quoted(source: &str, from: usize, key: &str) -> Option<u32> {
1326    let quoted = format!("\"{key}\"");
1327    let field = format!("{key}:");
1328    let (index, _) = source
1329        .lines()
1330        .enumerate()
1331        .skip(from)
1332        .find(|(_, l)| l.contains(&quoted) || l.trim_start().starts_with(&field))?;
1333    u32::try_from(index + 1).ok()
1334}