Skip to main content

shuyi_ui/input/
bindings.rs

1//! The player's key and button bindings (tickets 0217 and 0816, game ADR-0031,
2//! game ADR-0053): every rebindable action has [`SLOTS`] key slots and [`SLOTS`]
3//! controller-button slots. Each layout keeps its own keys
4//! ([`LayoutBindings`]); the buttons are one setup shared by both layouts
5//! ([`PadBindings`]); the lot is saved as [`PlayerKeys`]. Rules from
6//! `docs/design/controls.md`, *Rebinding keys* and *Rebinding buttons*.
7//! Pure: the game reads and writes the saved text. Generic over the game's
8//! [`ActionSet`] (ticket 1106).
9//!
10//! Invariants: a chord is in at most one slot of a [`LayoutBindings`]'s
11//! actions of each [group](ActionSet::Group), and no
12//! [reserved](LayoutBindings::is_reserved) chord is in any; a button is in
13//! at most one slot of a [`PadBindings`]'s actions of each group. Actions
14//! of different groups may share one (`docs/design/controls.md`, *Dialogue
15//! log*).
16
17use crate::prelude::*;
18use alloc::collections::{BTreeMap, BTreeSet};
19use core::fmt;
20
21use serde::{Deserialize, Serialize};
22
23use shuyi_assets::keymap::KeymapDef;
24use shuyi_assets::keymap::{ActionSet, ButtonChord, Chord, Layout, RepeatDef, SLOTS};
25
26use super::keymap::Keymap;
27use crate::DEBUG_TOOLS;
28
29/// One action's key slots; `None` is an empty slot.
30pub type Slots = [Option<Chord>; SLOTS];
31
32/// One action's controller-button slots, each a button or a
33/// [button chord](ButtonChord); `None` is an empty slot.
34pub type ButtonSlots = [Option<ButtonChord>; SLOTS];
35
36/// The saved form of a set of slots: action name → its slots' names.
37type Saved = BTreeMap<String, Vec<Option<String>>>;
38
39/// Every rebindable action's slots of keys or of buttons, with the rules
40/// both share: a thing is in at most one slot of each group's actions, and binding one that is already in a slot of that group moves
41/// it.
42#[derive(Debug, Clone, PartialEq, Eq)]
43struct SlotTable<A, T> {
44    slots: BTreeMap<A, [Option<T>; SLOTS]>,
45}
46
47impl<A: ActionSet, T: Copy + Eq> SlotTable<A, T> {
48    /// Every rebindable action, with empty slots.
49    fn empty() -> Self {
50        Self {
51            slots: A::ALL
52                .iter()
53                .copied()
54                .filter(|a| a.is_rebindable())
55                .map(|a| (a, [None; SLOTS]))
56                .collect(),
57        }
58    }
59
60    /// `action`'s slots (all empty for an action that has none).
61    fn get(&self, action: A) -> [Option<T>; SLOTS] {
62        self.slots.get(&action).copied().unwrap_or([None; SLOTS])
63    }
64
65    /// The slot of one of `group`'s actions holding `item`, if any.
66    fn find(&self, group: A::Group, item: T) -> Option<(A, usize)> {
67        let in_group = self.slots.iter().filter(|(a, _)| a.group() == group);
68        in_group.into_iter().find_map(|(&action, slots)| {
69            slots
70                .iter()
71                .position(|&c| c == Some(item))
72                .map(|i| (action, i))
73        })
74    }
75
76    /// Whether `action` has a slot `i`.
77    fn check(action: A, i: usize) -> Result<(), BindError<A>> {
78        if !action.is_rebindable() {
79            return Err(BindError::NotRebindable(action));
80        }
81        if i >= SLOTS {
82            return Err(BindError::NoSuchSlot(i));
83        }
84        Ok(())
85    }
86
87    /// Puts `item` in `action`'s slot `i`, replacing what was there. If it
88    /// was in another slot of `action`'s group, that slot is emptied and
89    /// returned.
90    fn place(&mut self, action: A, i: usize, item: T) -> Result<Option<(A, usize)>, BindError<A>> {
91        Self::check(action, i)?;
92        let from = self.find(action.group(), item);
93        if from == Some((action, i)) {
94            return Ok(None);
95        }
96        if let Some((other, j)) = from {
97            self.clear(other, j);
98        }
99        self.slots.entry(action).or_insert([None; SLOTS])[i] = Some(item);
100        Ok(from)
101    }
102
103    /// Empties `action`'s slot `i` (nothing happens for a slot that doesn't
104    /// exist).
105    fn clear(&mut self, action: A, i: usize) {
106        if let Some(slot) = self.slots.get_mut(&action).and_then(|s| s.get_mut(i)) {
107            *slot = None;
108        }
109    }
110
111    /// Whether every slot of `action` is empty.
112    fn is_unmapped(&self, action: A) -> bool {
113        self.get(action).iter().all(Option::is_none)
114    }
115
116    /// The [required](ActionSet::is_required) actions with every slot empty,
117    /// in [`ActionSet::ALL`] order.
118    fn unmapped_required(&self) -> Vec<A> {
119        A::ALL
120            .iter()
121            .copied()
122            .filter(|&a| a.is_required() && self.is_unmapped(a))
123            .collect()
124    }
125
126    /// Every filled slot with its action, in action then slot order.
127    fn pairs(&self) -> impl Iterator<Item = (T, A)> + '_ {
128        self.slots
129            .iter()
130            .flat_map(|(&action, slots)| slots.iter().flatten().map(move |&c| (c, action)))
131    }
132}
133
134impl<A: ActionSet, T: Copy + fmt::Display> SlotTable<A, T> {
135    /// The saved form: every action with all its slots.
136    fn saved(&self) -> Saved {
137        self.slots
138            .iter()
139            .map(|(action, slots)| {
140                let names = slots.iter().map(|c| c.map(|c| c.to_string())).collect();
141                (action.name().to_owned(), names)
142            })
143            .collect()
144    }
145}
146
147/// Why [`LayoutBindings::bind`] or [`PadBindings::bind`] refused.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
149pub enum BindError<A: ActionSet> {
150    /// The chord is fixed (`Escape`, `Delete`) or is the key of an action
151    /// that can't be rebound (Debug) in a build with debug tools.
152    #[error("{0} is a fixed key and can't be bound")]
153    Reserved(Chord),
154    /// The action can't be rebound (Debug).
155    #[error("{} can't be rebound", .0.name())]
156    NotRebindable(A),
157    /// Slot index past the last slot.
158    #[error("there is no key slot {0}")]
159    NoSuchSlot(usize),
160}
161
162/// One layout's player-edited bindings: every rebindable action's slots,
163/// plus the keys of the actions that can't be rebound (Debug), from the
164/// keymap file.
165///
166/// The pure editing API for the Key bindings screen (0815): [`bind`],
167/// [`clear`], [`defaults`] (restore defaults), [`unmapped_required`].
168///
169/// [`bind`]: Self::bind
170/// [`clear`]: Self::clear
171/// [`defaults`]: Self::defaults
172/// [`unmapped_required`]: Self::unmapped_required
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct LayoutBindings<A: ActionSet> {
175    /// Every rebindable action → its slots.
176    table: SlotTable<A, Chord>,
177    /// The chords of the actions that can't be rebound (Debug), as in the
178    /// keymap file.
179    fixed: Vec<(Chord, A)>,
180    /// Whether those chords are reserved (builds with debug tools,
181    /// game ADR-0023). Without debug tools a player may bind them, and their
182    /// slot wins over Debug.
183    fixed_reserved: bool,
184}
185
186impl<A: ActionSet> LayoutBindings<A> {
187    /// `layout`'s default keys from `keymap.ron`, each action's chords in
188    /// its slots in file order.
189    pub fn defaults(def: &KeymapDef<A>, layout: Layout) -> Self {
190        Self::from_def(def, layout, DEBUG_TOOLS)
191    }
192
193    /// [`defaults`](Self::defaults), with the chords of the actions that
194    /// can't be rebound reserved or not.
195    pub fn from_def(def: &KeymapDef<A>, layout: Layout, fixed_reserved: bool) -> Self {
196        let fixed = A::ALL.iter().filter(|a| !a.is_rebindable());
197        let mut bindings = Self {
198            table: SlotTable::empty(),
199            fixed: fixed
200                .flat_map(|&a| def.chords(layout, a).iter().map(move |&c| (c, a)))
201                .collect(),
202            fixed_reserved,
203        };
204        for (&action, chords) in def.layouts.get(&layout).into_iter().flatten() {
205            for (i, &chord) in chords.iter().take(SLOTS).enumerate() {
206                // A loaded keymap can't break the invariant; one built by
207                // hand is repaired the same way a player's bind would be.
208                bindings.bind(action, i, chord).ok();
209            }
210        }
211        bindings
212    }
213
214    /// `action`'s slots (all empty for Debug, which has none).
215    pub fn slots(&self, action: A) -> Slots {
216        self.table.get(action)
217    }
218
219    /// Whether `chord` can never be put in a slot: a [fixed chord] (plain
220    /// `Escape` or `Delete`; their `Shift+` chords are ordinary), or the
221    /// chord of an action that can't be rebound (Debug) in a build with
222    /// debug tools.
223    ///
224    /// [fixed chord]: Chord::is_reserved
225    pub fn is_reserved(&self, chord: Chord) -> bool {
226        chord.is_reserved() || (self.fixed_reserved && self.fixed.iter().any(|&(c, _)| c == chord))
227    }
228
229    /// The slot of one of `group`'s actions holding `chord`, if any.
230    pub fn find(&self, group: A::Group, chord: Chord) -> Option<(A, usize)> {
231        self.table.find(group, chord)
232    }
233
234    /// Puts `chord` in `action`'s slot `i`, replacing what was there. If
235    /// `chord` was in another slot (of any action of `action`'s
236    /// group, including `action`), that slot is emptied and
237    /// returned: the key *moves* (`docs/design/controls.md`), which may
238    /// leave that action with no key. An action of another group keeps it.
239    pub fn bind(
240        &mut self,
241        action: A,
242        i: usize,
243        chord: Chord,
244    ) -> Result<Option<(A, usize)>, BindError<A>> {
245        SlotTable::<A, Chord>::check(action, i)?;
246        self.allowed(chord)?;
247        self.table.place(action, i, chord)
248    }
249
250    /// [`BindError::Reserved`] if `chord` [is reserved](Self::is_reserved).
251    fn allowed(&self, chord: Chord) -> Result<(), BindError<A>> {
252        if self.is_reserved(chord) {
253            return Err(BindError::Reserved(chord));
254        }
255        Ok(())
256    }
257
258    /// Empties `action`'s slot `i` (nothing happens for a slot that doesn't
259    /// exist).
260    pub fn clear(&mut self, action: A, i: usize) {
261        self.table.clear(action, i);
262    }
263
264    /// Whether every slot of `action` is empty (shows `! not mapped`).
265    pub fn is_unmapped(&self, action: A) -> bool {
266        self.table.is_unmapped(action)
267    }
268
269    /// The [required](ActionSet::is_required) actions with no key, in
270    /// [`ActionSet::ALL`] order. The Key bindings screen can't be left while
271    /// this isn't empty.
272    pub fn unmapped_required(&self) -> Vec<A> {
273        self.table.unmapped_required()
274    }
275
276    /// The keymap these bindings give: every slot, the chords of the
277    /// actions that can't be rebound (unless a slot took one), and the
278    /// fixed keys ([`Keymap::new`]). Keys only:
279    /// controller buttons don't belong to a layout ([`PadBindings`],
280    /// [`Keymap::with_pad`]).
281    pub fn keymap(&self, repeat: RepeatDef) -> Keymap<A> {
282        let fixed = self.fixed.iter().copied();
283        // A chord given twice keeps its last action, so a slot beats Debug.
284        Keymap::new(fixed.chain(self.table.pairs()), repeat)
285    }
286}
287
288/// The player-edited controller buttons: every rebindable action's button
289/// slots, each holding a button or a [button chord](ButtonChord) (ticket
290/// 1034; a chord and its buttons on their own are different bindings).
291/// One setup for both keyboard layouts (`docs/design/controls.md`,
292/// *Rebinding buttons*); no button is reserved, and both sticks' directions
293/// count as buttons.
294///
295/// The pure editing API for the Key bindings screen (0816), the same shape
296/// as [`LayoutBindings`]: [`bind`](Self::bind), [`clear`](Self::clear),
297/// [`defaults`](Self::defaults) (restore defaults),
298/// [`unmapped_required`](Self::unmapped_required).
299#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct PadBindings<A: ActionSet> {
301    table: SlotTable<A, ButtonChord>,
302}
303
304impl<A: ActionSet> PadBindings<A> {
305    /// The default buttons from `keymap.ron`'s `pad` table, each action's
306    /// buttons in its slots in file order.
307    pub fn defaults(def: &KeymapDef<A>) -> Self {
308        let mut bindings = Self {
309            table: SlotTable::empty(),
310        };
311        for (&action, buttons) in &def.pad {
312            for (i, &button) in buttons.iter().take(SLOTS).enumerate() {
313                // As in `LayoutBindings::from_def`.
314                bindings.bind(action, i, button).ok();
315            }
316        }
317        bindings
318    }
319
320    /// `action`'s button slots (all empty for Debug, which has none).
321    pub fn slots(&self, action: A) -> ButtonSlots {
322        self.table.get(action)
323    }
324
325    /// The slot of one of `group`'s actions holding `button` (a button or
326    /// a button chord), if any.
327    pub fn find(&self, group: A::Group, button: impl Into<ButtonChord>) -> Option<(A, usize)> {
328        self.table.find(group, button.into())
329    }
330
331    /// Puts `button` (a button or a button chord) in `action`'s slot `i`,
332    /// replacing what was there. If `button` was in another slot (of any
333    /// action of `action`'s group, including `action`), that slot is
334    /// emptied and returned: the button *moves*, which may leave that
335    /// action with no button. An action of another group keeps it.
336    pub fn bind(
337        &mut self,
338        action: A,
339        i: usize,
340        button: impl Into<ButtonChord>,
341    ) -> Result<Option<(A, usize)>, BindError<A>> {
342        self.table.place(action, i, button.into())
343    }
344
345    /// Empties `action`'s slot `i` (nothing happens for a slot that doesn't
346    /// exist).
347    pub fn clear(&mut self, action: A, i: usize) {
348        self.table.clear(action, i);
349    }
350
351    /// Whether every slot of `action` is empty (shows `! not mapped`).
352    pub fn is_unmapped(&self, action: A) -> bool {
353        self.table.is_unmapped(action)
354    }
355
356    /// The [required](ActionSet::is_required) actions with no button, in
357    /// [`ActionSet::ALL`] order. The Key bindings screen can't be left while
358    /// this isn't empty.
359    pub fn unmapped_required(&self) -> Vec<A> {
360        self.table.unmapped_required()
361    }
362
363    /// Every bound button and button chord with its action, for
364    /// [`Keymap::with_pad`]: in action then slot order.
365    pub fn pairs(&self) -> impl Iterator<Item = (ButtonChord, A)> + '_ {
366        self.table.pairs()
367    }
368}
369
370/// Version written in, and required of, the saved config.
371pub const PLAYER_KEYS_VERSION: u32 = 2;
372
373/// The oldest saved version still read: version 1 (ticket 0217) had keys
374/// only, and loads with the default buttons.
375const OLDEST_READABLE_VERSION: u32 = 1;
376
377/// What repair warnings about the controller buttons start with (a
378/// layout's start with its name).
379const PAD_WARNING: &str = "controller";
380
381/// Every layout's player bindings and the controller buttons, saved under
382/// whatever name the game stores them under. A
383/// layout the player hasn't changed has no entry and uses its defaults, and
384/// so do the buttons. **Each layout keeps its own keys; the buttons are
385/// shared by every layout.**
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct PlayerKeys<A: ActionSet> {
388    layouts: BTreeMap<Layout, LayoutBindings<A>>,
389    /// The player's buttons; `None` while they are the defaults.
390    pad: Option<PadBindings<A>>,
391}
392
393impl<A: ActionSet> Default for PlayerKeys<A> {
394    fn default() -> Self {
395        Self {
396            layouts: BTreeMap::new(),
397            pad: None,
398        }
399    }
400}
401
402/// The saved form: action and chord names as text, so an unknown name
403/// drops only itself on load instead of the whole file.
404#[derive(Serialize, Deserialize)]
405#[serde(rename = "PlayerKeys")]
406struct PlayerKeysFile {
407    version: u32,
408    layouts: BTreeMap<String, Saved>,
409    /// The controller buttons, if the player changed them (since version
410    /// 2).
411    #[serde(default)]
412    pad: Option<Saved>,
413}
414
415/// Just the version, read first so a future format still gives a clear
416/// warning.
417#[derive(Deserialize)]
418#[serde(rename = "PlayerKeys")]
419struct VersionOnly {
420    version: u32,
421}
422
423impl<A: ActionSet> PlayerKeys<A> {
424    /// `layout`'s bindings: the player's, or the defaults if they haven't
425    /// changed that layout.
426    pub fn bindings(&self, def: &KeymapDef<A>, layout: Layout) -> LayoutBindings<A> {
427        self.layouts
428            .get(&layout)
429            .cloned()
430            .unwrap_or_else(|| LayoutBindings::defaults(def, layout))
431    }
432
433    /// The controller buttons: the player's, or the defaults if they
434    /// haven't changed them. The same whatever the layout.
435    pub fn pad_bindings(&self, def: &KeymapDef<A>) -> PadBindings<A> {
436        self.pad
437            .clone()
438            .unwrap_or_else(|| PadBindings::defaults(def))
439    }
440
441    /// The keymap for `layout` with the player's keys and buttons.
442    pub fn keymap(&self, def: &KeymapDef<A>, layout: Layout) -> Keymap<A> {
443        let keys = self.bindings(def, layout).keymap(def.repeat);
444        keys.with_pad(self.pad_bindings(def).pairs())
445            .with_chord(def.chord)
446    }
447
448    /// The keymap before any layout is chosen ([`Keymap::layout_picker`]),
449    /// with the player's buttons: someone playing only with a controller
450    /// never picks a layout.
451    pub fn layout_picker_keymap(&self, def: &KeymapDef<A>) -> Keymap<A> {
452        Keymap::layout_picker(def).with_pad(self.pad_bindings(def).pairs())
453    }
454
455    /// Replaces the controller buttons. Buttons equal to the defaults are
456    /// stored as "no entry", as in [`set`](Self::set).
457    pub fn set_pad(&mut self, def: &KeymapDef<A>, bindings: PadBindings<A>) {
458        self.pad = (bindings != PadBindings::defaults(def)).then_some(bindings);
459    }
460
461    /// Whether the player has changed the controller buttons.
462    pub fn is_custom_pad(&self) -> bool {
463        self.pad.is_some()
464    }
465
466    /// Replaces `layout`'s bindings; the other layout is untouched.
467    /// Bindings equal to the defaults are stored as "no entry", so a later
468    /// change to the default keys reaches a player who restored them.
469    pub fn set(&mut self, def: &KeymapDef<A>, layout: Layout, bindings: LayoutBindings<A>) {
470        if bindings == LayoutBindings::defaults(def, layout) {
471            self.layouts.remove(&layout);
472        } else {
473            self.layouts.insert(layout, bindings);
474        }
475    }
476
477    /// Whether the player has changed `layout`'s keys.
478    pub fn is_custom(&self, layout: Layout) -> bool {
479        self.layouts.contains_key(&layout)
480    }
481
482    /// The saved form (RON): every changed layout with all its actions'
483    /// slots, e.g. `"Confirm": [Some("f"), Some("Enter"), None]`, and the
484    /// buttons the same way under `pad` if changed, e.g. `"Confirm":
485    /// [Some("South"), None, None]`, a button chord as `"South+East"`
486    /// (ticket 1034: saved bindings without chords read as before).
487    pub fn to_ron(&self) -> String {
488        let layouts = self
489            .layouts
490            .iter()
491            .map(|(layout, bindings)| (layout.name().to_owned(), bindings.table.saved()))
492            .collect();
493        let file = PlayerKeysFile {
494            version: PLAYER_KEYS_VERSION,
495            layouts,
496            pad: self.pad.as_ref().map(|pad| pad.table.saved()),
497        };
498        let config = ron::ser::PrettyConfig::new()
499            .struct_names(true)
500            .compact_arrays(true);
501        // Strings, maps and options always serialise.
502        ron::ser::to_string_pretty(&file, config).unwrap_or_default()
503    }
504
505    /// Reads the saved form, repairing rather than failing, and returns
506    /// the result with a warning for everything it had to fix:
507    ///
508    /// - unreadable text or an unknown version: defaults for every layout
509    ///   and for the buttons;
510    /// - version 1 (keys only): the default buttons;
511    /// - layouts the keymap doesn't have, unknown actions, unreadable
512    ///   chords or buttons and extra slots are dropped;
513    /// - reserved chords are dropped;
514    /// - a chord in two stored slots of one group's actions stays in
515    ///   the first (in [`ActionSet::ALL`] and slot order); a default slot of
516    ///   that group holding a stored chord is emptied (the key moves, as
517    ///   with [`LayoutBindings::bind`]);
518    /// - an action the layout doesn't list keeps its default slots;
519    /// - a layout left with a required action unmapped goes back to its
520    ///   defaults;
521    /// - the buttons are repaired by the same rules as one layout's keys.
522    // check-text: not player text (warnings for the log)
523    pub fn from_ron(text: &str, def: &KeymapDef<A>) -> (Self, Vec<String>) {
524        let mut warnings = Vec::new();
525        let version = match ron::from_str::<VersionOnly>(text) {
526            Ok(v) => v.version,
527            Err(e) => {
528                warnings.push(format!("unreadable, using the default keys: {e}"));
529                return (Self::default(), warnings);
530            }
531        };
532        if !(OLDEST_READABLE_VERSION..=PLAYER_KEYS_VERSION).contains(&version) {
533            warnings.push(format!(
534                "version {version} isn't {PLAYER_KEYS_VERSION}, using the default keys"
535            ));
536            return (Self::default(), warnings);
537        }
538        let file = match ron::from_str::<PlayerKeysFile>(text) {
539            Ok(file) => file,
540            Err(e) => {
541                warnings.push(format!("unreadable, using the default keys: {e}"));
542                return (Self::default(), warnings);
543            }
544        };
545        let mut keys = Self::default();
546        for (name, actions) in &file.layouts {
547            let Some(layout) = Layout::from_name(name).filter(|&l| def.has_layout(l)) else {
548                warnings.push(format!("unknown layout \"{name}\" dropped"));
549                continue;
550            };
551            let defaults = LayoutBindings::defaults(def, layout);
552            let warn = |message: String| warnings.push(format!("{layout}: {message}"));
553            let allowed = |chord| defaults.allowed(chord);
554            let table = repair(&defaults.table, actions, "key", Chord::parse, allowed, warn);
555            keys.set(def, layout, LayoutBindings { table, ..defaults });
556        }
557        if let Some(actions) = &file.pad {
558            let defaults = PadBindings::defaults(def).table;
559            let warn = |message: String| warnings.push(format!("{PAD_WARNING}: {message}"));
560            let table = repair(
561                &defaults,
562                actions,
563                "button",
564                ButtonChord::parse,
565                |_| Ok(()),
566                warn,
567            );
568            keys.set_pad(def, PadBindings { table });
569        }
570        (keys, warnings)
571    }
572}
573
574/// The slots saved as `actions` over `defaults` (see
575/// [`PlayerKeys::from_ron`]), calling `warn` for each fix. `what` is what a
576/// slot holds ("key" or "button"), `parse` reads one from its saved name
577/// and `allowed` refuses the ones that may not be in a slot.
578fn repair<A: ActionSet, T: Copy + Ord + fmt::Display>(
579    defaults: &SlotTable<A, T>,
580    actions: &Saved,
581    what: &str,
582    parse: impl Fn(&str) -> Result<T, String>,
583    allowed: impl Fn(T) -> Result<(), BindError<A>>,
584    mut warn: impl FnMut(String),
585) -> SlotTable<A, T> {
586    let mut stored = BTreeMap::new();
587    for (name, slots) in actions {
588        match A::from_name(name).filter(|a| a.is_rebindable()) {
589            Some(action) => {
590                stored.insert(action, slots);
591            }
592            None => warn(format!("unknown action \"{name}\" dropped")),
593        }
594    }
595    let mut table = defaults.clone();
596    for &action in stored.keys() {
597        for i in 0..SLOTS {
598            table.clear(action, i);
599        }
600    }
601    let mut claimed = BTreeSet::new();
602    for (&action, slots) in &stored {
603        if slots.len() > SLOTS {
604            warn(format!(
605                "{}: {} slots, only the first {SLOTS} kept",
606                action.name(),
607                slots.len()
608            ));
609        }
610        for (i, name) in slots.iter().enumerate().take(SLOTS) {
611            let Some(name) = name else { continue };
612            let item = match parse(name) {
613                Ok(item) => item,
614                Err(e) => {
615                    warn(format!("{}: {e}", action.name()));
616                    continue;
617                }
618            };
619            if !claimed.insert((action.group(), item)) {
620                warn(format!(
621                    "{}: {item} is already on another action",
622                    action.name()
623                ));
624                continue;
625            }
626            if let Err(e) = allowed(item).and_then(|()| table.place(action, i, item)) {
627                warn(format!("{}: {e}", action.name()));
628            }
629        }
630    }
631    let unmapped = table.unmapped_required();
632    if unmapped.is_empty() {
633        table
634    } else {
635        let names: Vec<&str> = unmapped.iter().map(|a| a.name()).collect();
636        warn(format!(
637            "{} would have no {what}, using the default {what}s",
638            names.join(", ")
639        ));
640        defaults.clone()
641    }
642}
643
644#[cfg(test)]
645mod tests;