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;