Skip to main content

crcbl_input/
overrides.rs

1//! A player's rebinds as a diff over the game's declared defaults.
2//!
3//! The rule this keeps (the input rules in `docs/notes/simulation.md`): a
4//! player's rebinds are stored as **diffs over the defaults, never as a copy
5//! of the whole set**. [`ActionMap::overrides`] lists only the actions whose
6//! bindings differ from their declaration, so an action a game adds later, or
7//! a default it changes, reaches every player who never rebound that action.
8//! [`ActionMap::apply_overrides`] is the inverse. Where the list is stored,
9//! and in what file, is the game's business; each binding's text form is the
10//! [`Binding`] `Display`/`FromStr` pair.
11//!
12//! # As text
13//!
14//! [`ActionMap::override_text`] and [`ActionMap::apply_override_text`] are the
15//! same pair over that text form, for a store that keeps strings and knows
16//! nothing of bindings — `crcbl_store::profile` is one. One entry converts on
17//! its own, without a map, through [`ActionOverride::from_text`] and
18//! [`ActionOverride::texts`], which the pair is built on. Reading text is where
19//! a file written by another build meets this one, so each entry that cannot
20//! apply is refused on its own and named ([`OverrideRefusal`]): an action this
21//! build does not declare is skipped, and a binding text this build cannot
22//! read leaves its action on the defaults rather than on part of a list.
23
24use core::fmt;
25
26use crate::{ActionMap, ActionMapError, Binding, BindingParseError};
27
28/// One action whose bindings differ from its declaration.
29#[derive(Debug, Clone, PartialEq)]
30pub struct ActionOverride {
31    /// The action's name.
32    pub action: String,
33    /// The bindings the player chose, in place of the declared ones.
34    pub bindings: Vec<Binding>,
35}
36
37/// Why one entry of a saved list of rebinds was not applied — see
38/// [`ActionMap::apply_override_text`].
39#[derive(Debug, Clone, PartialEq)]
40pub enum OverrideRefusal {
41    /// The list names an action this map does not declare. The entry is
42    /// skipped and the rest apply.
43    UnknownAction(String),
44    /// One of an action's binding texts does not parse. The whole entry is
45    /// refused, so the action keeps its declared bindings rather than the
46    /// part of the player's list that did read.
47    BadBinding {
48        /// The action the entry was for.
49        action: String,
50        /// The text that did not parse, and why.
51        error: BindingParseError,
52    },
53    /// The map refused the bindings — a dead zone or threshold outside
54    /// `0.0..1.0` — and the action keeps its declared ones.
55    Refused(ActionMapError),
56}
57
58impl fmt::Display for OverrideRefusal {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::UnknownAction(action) => {
62                write!(f, "no action called `{action}`; its binds were skipped")
63            }
64            Self::BadBinding { action, error } => {
65                write!(f, "`{action}` keeps its defaults: {error}")
66            }
67            Self::Refused(error) => write!(f, "{error}; that action keeps its defaults"),
68        }
69    }
70}
71
72impl std::error::Error for OverrideRefusal {}
73
74impl ActionOverride {
75    /// One entry of a saved list in the bindings' text form, read without a
76    /// map: for a caller that loads its rebinds before its [`ActionMap`]
77    /// exists, or keeps the list itself. Whether `action` is declared is the
78    /// map's question, answered when the entry is applied.
79    ///
80    /// # Errors
81    ///
82    /// [`OverrideRefusal::BadBinding`] for the first text that does not parse:
83    /// the entry is refused whole, as [`ActionMap::apply_override_text`]
84    /// refuses it.
85    pub fn from_text(action: &str, texts: &[String]) -> Result<Self, OverrideRefusal> {
86        let bindings = texts
87            .iter()
88            .map(|text| text.parse::<Binding>())
89            .collect::<Result<Vec<_>, _>>()
90            .map_err(|error| OverrideRefusal::BadBinding {
91                action: action.to_owned(),
92                error,
93            })?;
94        Ok(Self {
95            action: action.to_owned(),
96            bindings,
97        })
98    }
99
100    /// The bindings' texts, in order: what [`Self::from_text`] reads back.
101    #[must_use]
102    pub fn texts(&self) -> Vec<String> {
103        self.bindings.iter().map(ToString::to_string).collect()
104    }
105}
106
107impl ActionMap {
108    /// Every action whose bindings differ from the ones it was declared with,
109    /// in declaration order, with its current bindings.
110    ///
111    /// An action rebound and then rebound back to its declaration is not
112    /// listed: the diff is by value, not by history.
113    #[must_use]
114    pub fn overrides(&self) -> Vec<ActionOverride> {
115        self.slots
116            .iter()
117            .filter(|slot| slot.decl.bindings != slot.defaults)
118            .map(|slot| ActionOverride {
119                action: slot.decl.name.clone(),
120                bindings: slot.decl.bindings.clone(),
121            })
122            .collect()
123    }
124
125    /// Sets the map to its declared defaults with `overrides` on top, through
126    /// [`ActionMap::rebind`]: each listed action gets its listed bindings and
127    /// **every action not listed goes back to its declaration**, so applying
128    /// what [`ActionMap::overrides`] returned reproduces the map it came from.
129    ///
130    /// An action whose bindings already match is left alone, so its value and
131    /// hold state survive. An override that cannot be applied is skipped and
132    /// its error returned, and the rest still apply: a settings file naming an
133    /// action the game no longer declares costs that one entry, not the load.
134    /// The returned list is empty when every override applied.
135    #[must_use = "an override that did not apply is reported only here"]
136    pub fn apply_overrides(&mut self, overrides: &[ActionOverride]) -> Vec<ActionMapError> {
137        let mut errors = Vec::new();
138        let mut wanted: Vec<Vec<Binding>> = self
139            .slots
140            .iter()
141            .map(|slot| slot.defaults.clone())
142            .collect();
143        for entry in overrides {
144            match self.name_to_idx.get(&entry.action) {
145                Some(&idx) if entry.bindings.iter().all(Binding::deadzone_is_valid) => {
146                    wanted[idx].clone_from(&entry.bindings);
147                }
148                Some(_) => errors.push(ActionMapError::InvalidDeadzone(entry.action.clone())),
149                None => errors.push(ActionMapError::UnknownAction(entry.action.clone())),
150            }
151        }
152        for (idx, bindings) in wanted.into_iter().enumerate() {
153            if self.slots[idx].decl.bindings != bindings {
154                let name = self.slots[idx].decl.name.clone();
155                if let Err(error) = self.rebind(&name, bindings) {
156                    errors.push(error);
157                }
158            }
159        }
160        errors
161    }
162
163    /// [`ActionMap::overrides`] in the bindings' text form: each rebound
164    /// action's name and its bindings' texts, in declaration order.
165    #[must_use]
166    pub fn override_text(&self) -> Vec<(String, Vec<String>)> {
167        self.overrides()
168            .into_iter()
169            .map(|entry| {
170                let texts = entry.texts();
171                (entry.action, texts)
172            })
173            .collect()
174    }
175
176    /// [`ActionMap::apply_overrides`] over a list in the bindings' text form,
177    /// with every entry that could not apply refused by name: an action this
178    /// map does not declare is skipped, and an entry with a binding text that
179    /// does not parse is refused whole — see [`OverrideRefusal`].
180    ///
181    /// Like `apply_overrides`, every action not listed goes back to its
182    /// declaration, and so does every action whose entry was refused.
183    #[must_use = "an entry that did not apply is reported only here"]
184    pub fn apply_override_text<'a>(
185        &mut self,
186        entries: impl IntoIterator<Item = (&'a str, &'a [String])>,
187    ) -> Vec<OverrideRefusal> {
188        let mut refusals = Vec::new();
189        let mut parsed = Vec::new();
190        for (action, texts) in entries {
191            match ActionOverride::from_text(action, texts) {
192                Ok(entry) => parsed.push(entry),
193                Err(refusal) => refusals.push(refusal),
194            }
195        }
196        refusals.extend(
197            self.apply_overrides(&parsed)
198                .into_iter()
199                .map(|error| match error {
200                    ActionMapError::UnknownAction(action) => OverrideRefusal::UnknownAction(action),
201                    other => OverrideRefusal::Refused(other),
202                }),
203        );
204        refusals
205    }
206
207    /// The action other than `action`, in the same context, that `binding` is
208    /// already one of the bindings of — what a rebind screen shows as a
209    /// conflict before it takes the input away from that action.
210    ///
211    /// Exact bindings only: a chord that shares a key with `binding` is not a
212    /// conflict here, because the chord rule already lets the two coexist.
213    /// Actions in other contexts are not conflicts either; the context stack
214    /// is what decides between them.
215    #[must_use]
216    pub fn bound_elsewhere(&self, action: &str, binding: &Binding) -> Option<&str> {
217        let context = self.context_of(action)?;
218        self.action_names().find(|other| {
219            *other != action
220                && self.context_of(other) == Some(context)
221                && self
222                    .bindings(other)
223                    .is_some_and(|bindings| bindings.contains(binding))
224        })
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use crcbl_core::input::KeyCode;
231
232    use super::*;
233    use crate::{ActionDecl, ActionKind, ActionValue, ButtonAction, ButtonState, Trigger};
234
235    fn map() -> ActionMap {
236        let mut map = ActionMap::new();
237        for (name, key) in [
238            ("jump", KeyCode::Space),
239            ("crouch", KeyCode::KeyC),
240            ("use", KeyCode::KeyE),
241        ] {
242            map.declare(ActionDecl {
243                name: name.to_owned(),
244                kind: ActionKind::Button,
245                bindings: vec![Binding::Key(key)],
246            });
247        }
248        map
249    }
250
251    fn one(action: &str, bindings: Vec<Binding>) -> ActionOverride {
252        ActionOverride {
253            action: action.to_owned(),
254            bindings,
255        }
256    }
257
258    #[test]
259    fn a_map_nobody_rebound_has_no_overrides() {
260        assert_eq!(map().overrides(), []);
261    }
262
263    #[test]
264    fn only_the_rebound_actions_are_listed_in_declaration_order() {
265        let mut map = map();
266        map.rebind("use", vec![Binding::Key(KeyCode::KeyF)])
267            .unwrap();
268        map.rebind("jump", vec![Binding::Key(KeyCode::KeyJ)])
269            .unwrap();
270        assert_eq!(
271            map.overrides(),
272            [
273                one("jump", vec![Binding::Key(KeyCode::KeyJ)]),
274                one("use", vec![Binding::Key(KeyCode::KeyF)]),
275            ]
276        );
277        // Rebound back by hand: the diff is by value.
278        map.rebind("use", vec![Binding::Key(KeyCode::KeyE)])
279            .unwrap();
280        assert_eq!(
281            map.overrides(),
282            [one("jump", vec![Binding::Key(KeyCode::KeyJ)])]
283        );
284    }
285
286    #[test]
287    fn applying_a_maps_overrides_to_a_fresh_map_reproduces_it() {
288        let mut played = map();
289        played
290            .rebind(
291                "crouch",
292                vec![
293                    Binding::Key(KeyCode::ControlLeft),
294                    Binding::PadTrigger {
295                        trigger: Trigger::Left,
296                        threshold: 0.25,
297                    },
298                ],
299            )
300            .unwrap();
301        let saved = played.overrides();
302
303        let mut fresh = map();
304        assert_eq!(fresh.apply_overrides(&saved), []);
305        assert_eq!(fresh.overrides(), saved);
306        assert_eq!(fresh.bindings("crouch"), played.bindings("crouch"));
307        assert_eq!(
308            fresh.bindings("jump"),
309            Some(&[Binding::Key(KeyCode::Space)][..])
310        );
311    }
312
313    #[test]
314    fn an_action_not_listed_goes_back_to_its_declaration() {
315        let mut map = map();
316        map.rebind("jump", vec![Binding::Key(KeyCode::KeyJ)])
317            .unwrap();
318        assert_eq!(map.apply_overrides(&[]), []);
319        assert_eq!(
320            map.bindings("jump"),
321            Some(&[Binding::Key(KeyCode::Space)][..])
322        );
323        assert_eq!(map.overrides(), []);
324    }
325
326    #[test]
327    fn a_bad_override_is_reported_and_the_rest_still_apply() {
328        let mut map = map();
329        let errors = map.apply_overrides(&[
330            one("retired_action", vec![Binding::Key(KeyCode::KeyR)]),
331            one(
332                "crouch",
333                vec![Binding::PadTrigger {
334                    trigger: Trigger::Left,
335                    threshold: 1.5,
336                }],
337            ),
338            one("use", vec![Binding::Key(KeyCode::KeyF)]),
339        ]);
340        assert_eq!(
341            errors,
342            [
343                ActionMapError::UnknownAction("retired_action".to_owned()),
344                ActionMapError::InvalidDeadzone("crouch".to_owned()),
345            ]
346        );
347        assert_eq!(
348            map.bindings("crouch"),
349            Some(&[Binding::Key(KeyCode::KeyC)][..])
350        );
351        assert_eq!(
352            map.overrides(),
353            [one("use", vec![Binding::Key(KeyCode::KeyF)])]
354        );
355    }
356
357    #[test]
358    fn an_action_already_as_asked_keeps_its_hold() {
359        let mut map = map();
360        map.key_event(KeyCode::Space, true);
361        map.begin_tick(0.016);
362        assert_eq!(map.apply_overrides(&[]), []);
363        // A rebind resets hold state, and would read as a fresh press with no
364        // time held; jump was not rebound, so its hold carries on.
365        let ActionValue::Button(ButtonAction {
366            state: ButtonState::Held { duration },
367            just_pressed,
368            ..
369        }) = *map.action("jump").unwrap()
370        else {
371            panic!("jump is a held button");
372        };
373        assert!(!just_pressed, "pressed again by a rebind nobody asked for");
374        assert!(duration > 0.0, "the hold restarted: {duration}");
375    }
376
377    #[test]
378    fn overrides_round_trip_through_their_text() {
379        let mut map = map();
380        map.rebind(
381            "use",
382            vec![
383                Binding::Key(KeyCode::KeyF),
384                Binding::PadButton(crate::PadButton::West),
385            ],
386        )
387        .unwrap();
388        let written: Vec<(String, Vec<String>)> = map
389            .overrides()
390            .into_iter()
391            .map(|entry| (entry.action.clone(), entry.texts()))
392            .collect();
393        assert_eq!(
394            written,
395            [(
396                "use".to_owned(),
397                vec!["KeyF".to_owned(), "Pad:West".to_owned()]
398            )]
399        );
400        // Read back with no map in hand, as a game loading its settings
401        // before its map exists does.
402        let read: Vec<ActionOverride> = written
403            .iter()
404            .map(|(action, texts)| ActionOverride::from_text(action, texts).unwrap())
405            .collect();
406        let mut fresh = self::tests::map();
407        assert_eq!(fresh.apply_overrides(&read), []);
408        assert_eq!(fresh.overrides(), map.overrides());
409    }
410
411    /// One entry read with no map: an action name is taken as given, and a
412    /// text that does not parse refuses the whole entry by name.
413    #[test]
414    fn one_entry_reads_without_a_map_and_a_bad_text_refuses_it() {
415        let entry =
416            ActionOverride::from_text("anything", &["KeyF".to_owned(), "Pad:West".to_owned()])
417                .unwrap();
418        assert_eq!(entry.action, "anything");
419        assert_eq!(
420            entry.bindings,
421            [
422                Binding::Key(KeyCode::KeyF),
423                Binding::PadButton(crate::PadButton::West)
424            ]
425        );
426        assert_eq!(entry.texts(), ["KeyF", "Pad:West"]);
427        let refused =
428            ActionOverride::from_text("use", &["KeyF".to_owned(), "NoSuchKey".to_owned()]);
429        assert!(
430            matches!(
431                &refused,
432                Err(OverrideRefusal::BadBinding { action, .. }) if action == "use"
433            ),
434            "{refused:?}"
435        );
436    }
437
438    /// A button chord saved as text comes back as the chord, and still takes
439    /// its button from the plain binding once applied.
440    #[test]
441    fn a_button_chord_override_round_trips_and_still_shadows() {
442        use crcbl_core::input::PointerButton;
443
444        let mut map = map();
445        map.declare(ActionDecl {
446            name: "aim".to_owned(),
447            kind: ActionKind::Button,
448            bindings: vec![Binding::MouseButton(PointerButton::Right)],
449        });
450        let chord = Binding::ButtonChord {
451            modifier: crate::Modifier::Alt,
452            button: PointerButton::Right,
453        };
454        map.rebind("use", vec![chord.clone()]).unwrap();
455        let written: Vec<String> = map.overrides()[0]
456            .bindings
457            .iter()
458            .map(ToString::to_string)
459            .collect();
460        assert_eq!(written, ["Alt+Mouse:Right"]);
461
462        let mut fresh = self::tests::map();
463        fresh.declare(ActionDecl {
464            name: "aim".to_owned(),
465            kind: ActionKind::Button,
466            bindings: vec![Binding::MouseButton(PointerButton::Right)],
467        });
468        let read = one("use", written.iter().map(|t| t.parse().unwrap()).collect());
469        assert_eq!(fresh.apply_overrides(&[read]), []);
470        assert_eq!(fresh.bindings("use"), Some(&[chord][..]));
471
472        fresh.key_event(KeyCode::AltLeft, true);
473        fresh.mouse_button(PointerButton::Right, true);
474        assert!(fresh.button_held("use"));
475        assert!(!fresh.button_held("aim"), "the loaded chord did not shadow");
476    }
477
478    fn texts(entries: &[(&str, &[&str])]) -> Vec<(String, Vec<String>)> {
479        entries
480            .iter()
481            .map(|(action, texts)| {
482                (
483                    (*action).to_owned(),
484                    texts.iter().map(|text| (*text).to_owned()).collect(),
485                )
486            })
487            .collect()
488    }
489
490    fn apply_text(map: &mut ActionMap, entries: &[(String, Vec<String>)]) -> Vec<OverrideRefusal> {
491        map.apply_override_text(
492            entries
493                .iter()
494                .map(|(action, texts)| (action.as_str(), texts.as_slice())),
495        )
496    }
497
498    /// **The text form round-trips**: a map's rebinds as text, applied to a
499    /// fresh map, reproduce it, an action left with nothing included.
500    #[test]
501    fn override_text_round_trips_through_a_fresh_map() {
502        let mut played = map();
503        played
504            .rebind(
505                "use",
506                vec![
507                    Binding::Key(KeyCode::KeyF),
508                    Binding::PadButton(crate::PadButton::West),
509                ],
510            )
511            .unwrap();
512        played.rebind("crouch", Vec::new()).unwrap();
513        let saved = played.override_text();
514        assert_eq!(
515            saved,
516            texts(&[("crouch", &[]), ("use", &["KeyF", "Pad:West"])])
517        );
518
519        let mut fresh = map();
520        assert_eq!(apply_text(&mut fresh, &saved), []);
521        assert_eq!(fresh.overrides(), played.overrides());
522    }
523
524    /// **An action this build does not declare is skipped and named**, and
525    /// the entries beside it still apply.
526    #[test]
527    fn an_unknown_action_in_the_text_is_skipped_by_name() {
528        let mut map = map();
529        let refusals = apply_text(
530            &mut map,
531            &texts(&[("glide", &["KeyG"]), ("jump", &["KeyJ"])]),
532        );
533        assert_eq!(
534            refusals,
535            [OverrideRefusal::UnknownAction("glide".to_owned())]
536        );
537        assert!(refusals[0].to_string().contains("glide"));
538        assert_eq!(
539            map.bindings("jump"),
540            Some(&[Binding::Key(KeyCode::KeyJ)][..])
541        );
542    }
543
544    /// **A binding text naming no key is refused by name**, and its action
545    /// keeps its defaults whole rather than the half of the list that read.
546    #[test]
547    fn a_bad_key_name_is_refused_by_name_and_its_action_keeps_its_defaults() {
548        let mut map = map();
549        let refusals = apply_text(
550            &mut map,
551            &texts(&[("jump", &["KeyJ", "KeyQwerty"]), ("use", &["KeyF"])]),
552        );
553        let [OverrideRefusal::BadBinding { action, error }] = refusals.as_slice() else {
554            panic!("expected one bad binding, got {refusals:?}");
555        };
556        assert_eq!(action, "jump");
557        assert_eq!(error.text, "KeyQwerty");
558        assert!(
559            refusals[0].to_string().contains("KeyQwerty"),
560            "the refusal does not name the text: {}",
561            refusals[0]
562        );
563        assert_eq!(
564            map.bindings("jump"),
565            Some(&[Binding::Key(KeyCode::Space)][..]),
566            "half a list was applied"
567        );
568        assert_eq!(
569            map.bindings("use"),
570            Some(&[Binding::Key(KeyCode::KeyF)][..])
571        );
572    }
573
574    /// **Rebinds saved by one build reach the next one, which added an
575    /// action**: the new action comes up on its declared default, because the
576    /// saved text is a diff and says nothing about it.
577    #[test]
578    fn saved_rebinds_survive_a_new_default_action() {
579        let mut old = map();
580        old.rebind("jump", vec![Binding::Key(KeyCode::KeyJ)])
581            .unwrap();
582        let saved = old.override_text();
583
584        let mut updated = map();
585        updated.declare(ActionDecl {
586            name: "dash".to_owned(),
587            kind: ActionKind::Button,
588            bindings: vec![Binding::Key(KeyCode::ShiftLeft)],
589        });
590        assert_eq!(apply_text(&mut updated, &saved), []);
591        assert_eq!(
592            updated.bindings("dash"),
593            Some(&[Binding::Key(KeyCode::ShiftLeft)][..]),
594            "the new action did not come up on its default"
595        );
596        assert_eq!(
597            updated.bindings("jump"),
598            Some(&[Binding::Key(KeyCode::KeyJ)][..])
599        );
600        assert_eq!(updated.override_text(), saved);
601    }
602
603    /// **A binding is a conflict only with another action in the same
604    /// context**: not with the action itself, and not across contexts.
605    #[test]
606    fn a_binding_is_bound_elsewhere_only_in_the_same_context() {
607        let mut map = map();
608        map.declare_in(
609            "vehicle",
610            ActionDecl {
611                name: "horn".to_owned(),
612                kind: ActionKind::Button,
613                bindings: vec![Binding::Key(KeyCode::KeyH)],
614            },
615        );
616        let space = Binding::Key(KeyCode::Space);
617        assert_eq!(map.bound_elsewhere("use", &space), Some("jump"));
618        assert_eq!(map.bound_elsewhere("jump", &space), None, "itself");
619        assert_eq!(map.bound_elsewhere("horn", &space), None, "another context");
620        assert_eq!(
621            map.bound_elsewhere("use", &Binding::Key(KeyCode::KeyH)),
622            None
623        );
624        assert_eq!(
625            map.bound_elsewhere("use", &Binding::Key(KeyCode::KeyQ)),
626            None
627        );
628    }
629}