Skip to main content

crcbl_input/
binding_asset.rs

1//! A game's default actions as a RON file: [`ActionMap::from_ron`] reads one,
2//! [`ActionMap::to_ron`] writes one.
3//!
4//! ```
5//! use crcbl_input::ActionMap;
6//!
7//! let map = ActionMap::from_ron(
8//!     r#"[
9//!     (action: "move", kind: Axis2, keyboard: ["Wasd:KeyW,KeyS,KeyA,KeyD"], gamepad: ["PadStick:Left>0.2"]),
10//!     (action: "jump", kind: Button, keyboard: ["Space"], gamepad: ["Pad:South"], patterns: [Hold(400, "jump_charge")]),
11//!     (action: "jump_charge", kind: Button),
12//! ]"#,
13//! )?;
14//! assert_eq!(map.emits("jump", crcbl_input::Pattern::Hold), Some("jump_charge"));
15//! # Ok::<(), crcbl_input::BindingAssetError>(())
16//! ```
17//!
18//! # The schema
19//!
20//! A list of records, one per action, declared in file order:
21//!
22//! - `action` — the name, unique across the file.
23//! - `kind` — `Button`, `Axis1` or `Axis2`, the [`ActionKind`].
24//! - `context` — the context it is declared in; left out, it is
25//!   [`GAMEPLAY_CONTEXT`].
26//! - `keyboard`, `mouse`, `gamepad`, `touch` — its bindings, by the kind of
27//!   [`Device`] each listens to ([`Binding::device`]; `mouse` is
28//!   [`Device::Pointer`]'s list). Each binding is written in its **text form**
29//!   (`binding_text.rs`), the one a player's saved rebinds use, so a file, a
30//!   rebind and this asset cannot spell a binding two ways.
31//! - `patterns` — `Tap(ms, "name")`, `Hold(ms, "name")`,
32//!   `DoubleTap(tap_ms, window_ms, "name")` and
33//!   `DoubleTapOnRelease(tap_ms, window_ms, "name")`, at most one tap, one
34//!   hold and one double tap, timed in whole milliseconds. Each **emits** the
35//!   named action when it fires (`emit.rs`), and that action must be a
36//!   `Button` declared in the same file.
37//! - `pad_chords_outrank` — `true` lets the action's pad chords take their
38//!   button from the contexts above it ([`ActionMap::set_pad_chords_outrank`]);
39//!   left out, it is `false`.
40//!
41//! Every field but `action` and `kind` may be left out.
42//!
43//! # The device lists are presentation
44//!
45//! An action's bindings are one flat list, and nothing downstream can tell
46//! which binding spoke (`docs/notes/simulation.md`, _What the deleted 19-input
47//! plan left behind_). The four lists are only how a file groups them: they are
48//! flattened into [`ActionDecl::bindings`] in the fixed order
49//! [`DEVICE_LISTS`] gives — keyboard, mouse, gamepad, touch — whatever order
50//! the file writes the fields in. A binding in a list that is not its device's
51//! is refused by name, so a key cannot hide under `gamepad`.
52//!
53//! # Defaults, not the player's choices
54//!
55//! What a file declares is each action's **defaults**, the ones a player's
56//! rebinds are a diff over (`overrides.rs`): [`ActionMap::overrides`] and
57//! [`ActionMap::apply_overrides`] work on a loaded map as on one declared in
58//! code, and [`ActionMap::to_ron`] writes the defaults whatever the player
59//! rebound.
60//!
61//! # Refused at the boundary
62//!
63//! A file is untrusted input, and everything wrong with it is a
64//! [`BindingAssetError`] naming the line and column and what was wrong — never
65//! a panic, and never a map with the bad record left out. A refusal of a record
66//! as a whole (a duplicate name, a binding in the wrong list, a pattern naming
67//! an action the file does not declare) points at the end of that record.
68
69use std::collections::HashMap;
70use std::fmt;
71
72use serde::de::{self, DeserializeSeed, SeqAccess, Visitor};
73use serde::{Deserialize, Serialize};
74
75use crate::{
76    ActionDecl, ActionKind, ActionMap, ActionMapError, Binding, BindingParseError, Device,
77    DoubleTap, GAMEPLAY_CONTEXT, Hold, Pattern, Tap,
78};
79
80/// The devices a record groups its bindings by, in the order their lists are
81/// flattened into one binding list.
82pub const DEVICE_LISTS: [Device; 4] = [
83    Device::Keyboard,
84    Device::Pointer,
85    Device::Gamepad,
86    Device::Touch,
87];
88
89/// The field a device's bindings are written under.
90#[must_use]
91pub const fn list_name(device: Device) -> &'static str {
92    match device {
93        Device::Keyboard => "keyboard",
94        Device::Pointer => "mouse",
95        Device::Gamepad => "gamepad",
96        Device::Touch => "touch",
97    }
98}
99
100/// A pattern's time in the file is whole milliseconds; the map times it in
101/// seconds.
102const MS_PER_SECOND: f32 = 1000.0;
103
104impl ActionMap {
105    /// A map declaring every action a binding asset lists, in file order, with
106    /// its patterns attached — see the [module docs](self) for the schema.
107    ///
108    /// # Errors
109    /// [`BindingAssetError`] for text that is not RON, RON that is not the
110    /// schema (an unknown field or kind, a missing `action`), or a schema that
111    /// is not a usable map; [`AssetRefusal`] names each.
112    pub fn from_ron(text: &str) -> Result<Self, BindingAssetError> {
113        // A first pass for every action's name and kind, so a pattern can name
114        // an action declared further down and still be refused where it is
115        // written rather than at the end of the file.
116        let headers: Vec<Header> = ron::from_str(text).map_err(BindingAssetError::parse)?;
117        let mut kinds = HashMap::with_capacity(headers.len());
118        for header in headers {
119            kinds.entry(header.action).or_insert(header.kind);
120        }
121        let mut map = Self::new();
122        let mut refusal = None;
123        let read = ron::Options::default().from_str_seed(
124            text,
125            Records {
126                kinds: &kinds,
127                map: &mut map,
128                refusal: &mut refusal,
129            },
130        );
131        match read {
132            Ok(()) => Ok(map),
133            Err(error) => Err(match refusal {
134                Some(refusal) => BindingAssetError::at(&error, refusal),
135                None => BindingAssetError::parse(error),
136            }),
137        }
138    }
139
140    /// Every action's declaration as a binding asset, in declaration order, in
141    /// the canonical form: fields in the schema's order, empty lists left out,
142    /// four-space indent, `\n` newlines on every platform, and a final
143    /// newline. Reading a canonical file and writing it back gives the same
144    /// bytes.
145    ///
146    /// Writes each action's **defaults**, not what a player rebound them to —
147    /// see the [module docs](self). An emitted action named on a pattern that
148    /// is not attached is not written, since it does nothing.
149    ///
150    /// # Errors
151    /// [`AssetWriteError`] for a map the schema cannot say: one whose reading
152    /// back would differ from it.
153    ///
154    /// # Panics
155    /// Never, in practice: a record is strings, integers, enums and
156    /// sequences, and ron's serializer has no failing path over those.
157    pub fn to_ron(&self) -> Result<String, AssetWriteError> {
158        let records = self
159            .slots
160            .iter()
161            .map(|slot| self.record(slot))
162            .collect::<Result<Vec<_>, _>>()?;
163        let pretty = ron::ser::PrettyConfig::new()
164            .new_line("\n")
165            .indentor("    ")
166            // Lists of records one per line, and everything inside a record
167            // on that record's lines: two levels, then compact.
168            .depth_limit(2);
169        let mut text = ron::ser::to_string_pretty(&records, pretty)
170            .expect("a binding asset has no serializer path that can fail");
171        text.push('\n');
172        Ok(text)
173    }
174
175    /// One slot as its record.
176    fn record(&self, slot: &crate::ActionSlot) -> Result<ActionFile, AssetWriteError> {
177        let name = &slot.decl.name;
178        if slot.repeat.is_some() {
179            return Err(AssetWriteError::Repeat(name.clone()));
180        }
181        let mut file = ActionFile {
182            action: name.clone(),
183            kind: slot.decl.kind.into(),
184            context: self.contexts[slot.context].clone(),
185            keyboard: Vec::new(),
186            mouse: Vec::new(),
187            gamepad: Vec::new(),
188            touch: Vec::new(),
189            patterns: Vec::new(),
190            pad_chords_outrank: slot.pad_chords_outrank,
191        };
192        for binding in &slot.defaults {
193            file.list_mut(binding.device()).push(binding.to_string());
194        }
195        let read_back = DEVICE_LISTS
196            .iter()
197            .flat_map(|device| file.list(*device))
198            .map(String::as_str);
199        if !read_back.eq(slot.defaults.iter().map(ToString::to_string)) {
200            return Err(AssetWriteError::MixedLists(name.clone()));
201        }
202        let emits = |pattern| {
203            self.emits(name, pattern)
204                .map(str::to_owned)
205                .ok_or_else(|| AssetWriteError::NoEmit {
206                    action: name.clone(),
207                    pattern,
208                })
209        };
210        let ms = |seconds, pattern| {
211            whole_ms(seconds).ok_or_else(|| AssetWriteError::NotWholeMilliseconds {
212                action: name.clone(),
213                pattern,
214            })
215        };
216        if let Some(tap) = self.tap(name) {
217            let time = ms(tap.time(), Pattern::Tap)?;
218            file.patterns
219                .push(PatternFile::Tap(time, emits(Pattern::Tap)?));
220        }
221        if let Some(hold) = self.hold(name) {
222            let time = ms(hold.time(), Pattern::Hold)?;
223            file.patterns
224                .push(PatternFile::Hold(time, emits(Pattern::Hold)?));
225        }
226        if let Some(double) = self.double_tap(name) {
227            let tap_time = ms(double.tap_time(), Pattern::DoubleTap)?;
228            let window = ms(double.window(), Pattern::DoubleTap)?;
229            let target = emits(Pattern::DoubleTap)?;
230            file.patterns.push(if double.fires_on_release() {
231                PatternFile::DoubleTapOnRelease(tap_time, window, target)
232            } else {
233                PatternFile::DoubleTap(tap_time, window, target)
234            });
235        }
236        Ok(file)
237    }
238}
239
240/// `seconds` as whole milliseconds, if it is exactly that many: what
241/// [`ActionMap::from_ron`] reads back has to be the same `f32`.
242fn whole_ms(seconds: f32) -> Option<u16> {
243    // `as` saturates out of range and maps NaN to zero; the comparison below
244    // refuses anything it changed.
245    let ms = (seconds * MS_PER_SECOND).round() as u16;
246    (f32::from(ms) / MS_PER_SECOND == seconds).then_some(ms)
247}
248
249/// The patterns' file spelling.
250const fn pattern_name(pattern: Pattern) -> &'static str {
251    match pattern {
252        Pattern::Tap => "Tap",
253        Pattern::Hold => "Hold",
254        Pattern::DoubleTap => "DoubleTap",
255    }
256}
257
258// ---------------------------------------------------------------------------
259// The file's shape
260// ---------------------------------------------------------------------------
261
262/// One action's record. Field order is the canonical order [`ActionMap::to_ron`]
263/// writes.
264#[derive(Serialize, Deserialize)]
265#[serde(rename = "Action", deny_unknown_fields)]
266struct ActionFile {
267    action: String,
268    kind: KindFile,
269    #[serde(default = "gameplay", skip_serializing_if = "is_gameplay")]
270    context: String,
271    #[serde(default, skip_serializing_if = "Vec::is_empty")]
272    keyboard: Vec<String>,
273    #[serde(default, skip_serializing_if = "Vec::is_empty")]
274    mouse: Vec<String>,
275    #[serde(default, skip_serializing_if = "Vec::is_empty")]
276    gamepad: Vec<String>,
277    #[serde(default, skip_serializing_if = "Vec::is_empty")]
278    touch: Vec<String>,
279    #[serde(default, skip_serializing_if = "Vec::is_empty")]
280    patterns: Vec<PatternFile>,
281    #[serde(default, skip_serializing_if = "is_false")]
282    pad_chords_outrank: bool,
283}
284
285impl ActionFile {
286    fn list(&self, device: Device) -> &[String] {
287        match device {
288            Device::Keyboard => &self.keyboard,
289            Device::Pointer => &self.mouse,
290            Device::Gamepad => &self.gamepad,
291            Device::Touch => &self.touch,
292        }
293    }
294
295    fn list_mut(&mut self, device: Device) -> &mut Vec<String> {
296        match device {
297            Device::Keyboard => &mut self.keyboard,
298            Device::Pointer => &mut self.mouse,
299            Device::Gamepad => &mut self.gamepad,
300            Device::Touch => &mut self.touch,
301        }
302    }
303}
304
305fn gameplay() -> String {
306    GAMEPLAY_CONTEXT.to_owned()
307}
308
309fn is_gameplay(context: &str) -> bool {
310    context == GAMEPLAY_CONTEXT
311}
312
313fn is_false(flag: &bool) -> bool {
314    !flag
315}
316
317/// The first pass's view of a record: what a pattern needs to know about the
318/// action it emits. Everything else is skipped here and checked in the second.
319#[derive(Deserialize)]
320#[serde(rename = "Action")]
321struct Header {
322    action: String,
323    kind: KindFile,
324}
325
326/// [`ActionKind`]'s file spelling.
327#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
328#[serde(rename = "Kind")]
329enum KindFile {
330    Button,
331    Axis1,
332    Axis2,
333}
334
335impl From<ActionKind> for KindFile {
336    fn from(kind: ActionKind) -> Self {
337        match kind {
338            ActionKind::Button => Self::Button,
339            ActionKind::Axis1 => Self::Axis1,
340            ActionKind::Axis2 => Self::Axis2,
341        }
342    }
343}
344
345impl From<KindFile> for ActionKind {
346    fn from(kind: KindFile) -> Self {
347        match kind {
348            KindFile::Button => Self::Button,
349            KindFile::Axis1 => Self::Axis1,
350            KindFile::Axis2 => Self::Axis2,
351        }
352    }
353}
354
355/// One pattern, its times in milliseconds and the action it emits.
356#[derive(Serialize, Deserialize)]
357#[serde(rename = "Pattern")]
358enum PatternFile {
359    Tap(u16, String),
360    Hold(u16, String),
361    DoubleTap(u16, u16, String),
362    DoubleTapOnRelease(u16, u16, String),
363}
364
365impl PatternFile {
366    const fn pattern(&self) -> Pattern {
367        match self {
368            Self::Tap(..) => Pattern::Tap,
369            Self::Hold(..) => Pattern::Hold,
370            Self::DoubleTap(..) | Self::DoubleTapOnRelease(..) => Pattern::DoubleTap,
371        }
372    }
373
374    fn emits(&self) -> &str {
375        match self {
376            Self::Tap(_, emits)
377            | Self::Hold(_, emits)
378            | Self::DoubleTap(_, _, emits)
379            | Self::DoubleTapOnRelease(_, _, emits) => emits,
380        }
381    }
382
383    /// This pattern's times as the map counts them, or `None` for a time of
384    /// zero.
385    fn timed(&self) -> Option<Timed> {
386        let seconds = |ms: u16| f32::from(ms) / MS_PER_SECOND;
387        Some(match *self {
388            Self::Tap(ms, _) => Timed::Tap(Tap::new(seconds(ms))?),
389            Self::Hold(ms, _) => Timed::Hold(Hold::new(seconds(ms))?),
390            Self::DoubleTap(tap, window, _) => {
391                Timed::DoubleTap(DoubleTap::new(seconds(tap), seconds(window))?)
392            }
393            Self::DoubleTapOnRelease(tap, window, _) => {
394                Timed::DoubleTap(DoubleTap::new(seconds(tap), seconds(window))?.on_release())
395            }
396        })
397    }
398}
399
400/// A pattern whose times have been checked, waiting for every action to be
401/// declared before it is attached.
402enum Timed {
403    Tap(Tap),
404    Hold(Hold),
405    DoubleTap(DoubleTap),
406}
407
408impl Timed {
409    /// Attach this pattern to `action`, emitting `emits`.
410    fn attach(self, map: &mut ActionMap, action: &str, emits: &str) -> Result<(), ActionMapError> {
411        let pattern = match self {
412            Self::Tap(tap) => {
413                map.set_tap(action, Some(tap))?;
414                Pattern::Tap
415            }
416            Self::Hold(hold) => {
417                map.set_hold(action, Some(hold))?;
418                Pattern::Hold
419            }
420            Self::DoubleTap(double) => {
421                map.set_double_tap(action, Some(double))?;
422                Pattern::DoubleTap
423            }
424        };
425        map.set_emits(action, pattern, Some(emits))
426    }
427}
428
429// ---------------------------------------------------------------------------
430// The second pass
431// ---------------------------------------------------------------------------
432
433/// The second pass over the file's list: each record checked and declared as
434/// it is read, so a refusal carries the position ron is at.
435struct Records<'a> {
436    /// Every action's kind, from the first pass.
437    kinds: &'a HashMap<String, KindFile>,
438    map: &'a mut ActionMap,
439    /// What refused the file, for [`ActionMap::from_ron`] to report: serde
440    /// carries a visitor's error only as a message.
441    refusal: &'a mut Option<AssetRefusal>,
442}
443
444impl Records<'_> {
445    /// Check one record and declare it. Its patterns are attached once every
446    /// action is declared, since one may emit an action further down.
447    fn declare(&mut self, record: ActionFile) -> Result<Vec<(Timed, String)>, AssetRefusal> {
448        let action = record.action.clone();
449        let mut bindings = Vec::new();
450        for device in DEVICE_LISTS {
451            for text in record.list(device) {
452                let binding =
453                    text.parse::<Binding>()
454                        .map_err(|error| AssetRefusal::BadBinding {
455                            action: action.clone(),
456                            error,
457                        })?;
458                if binding.device() != device {
459                    return Err(AssetRefusal::WrongList {
460                        action,
461                        binding: text.clone(),
462                        list: device,
463                    });
464                }
465                bindings.push(binding);
466            }
467        }
468        let mut seen = [false; Pattern::ALL.len()];
469        let mut timed = Vec::with_capacity(record.patterns.len());
470        for pattern in &record.patterns {
471            let kind = pattern.pattern();
472            let emits = pattern.emits();
473            let Some(times) = pattern.timed() else {
474                return Err(AssetRefusal::BadTime {
475                    action,
476                    pattern: kind,
477                });
478            };
479            timed.push((times, emits.to_owned()));
480            if std::mem::replace(&mut seen[kind.index()], true) {
481                return Err(AssetRefusal::DuplicatePattern {
482                    action,
483                    pattern: kind,
484                });
485            }
486            if emits == action {
487                return Err(AssetRefusal::EmitsItself {
488                    action,
489                    pattern: kind,
490                });
491            }
492            match self.kinds.get(emits) {
493                Some(KindFile::Button) => {}
494                Some(_) => {
495                    return Err(AssetRefusal::EmitNotAButton {
496                        action,
497                        pattern: kind,
498                        emits: emits.to_owned(),
499                    });
500                }
501                None => {
502                    return Err(AssetRefusal::UnknownEmit {
503                        action,
504                        pattern: kind,
505                        emits: emits.to_owned(),
506                    });
507                }
508            }
509        }
510        self.map
511            .try_declare_in(
512                &record.context,
513                ActionDecl {
514                    name: record.action,
515                    kind: record.kind.into(),
516                    bindings,
517                },
518            )
519            .and_then(|()| {
520                self.map
521                    .set_pad_chords_outrank(&action, record.pad_chords_outrank)
522            })
523            .map_err(AssetRefusal::Declare)?;
524        Ok(timed)
525    }
526
527    /// Hand `refusal` to [`ActionMap::from_ron`] and stop the parse where it is.
528    fn refuse<E: de::Error>(&mut self, refusal: AssetRefusal) -> E {
529        let error = E::custom(&refusal);
530        *self.refusal = Some(refusal);
531        error
532    }
533}
534
535impl<'de> DeserializeSeed<'de> for Records<'_> {
536    type Value = ();
537
538    fn deserialize<D: de::Deserializer<'de>>(self, deserializer: D) -> Result<(), D::Error> {
539        deserializer.deserialize_seq(self)
540    }
541}
542
543impl<'de> Visitor<'de> for Records<'_> {
544    type Value = ();
545
546    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547        f.write_str("a list of action records")
548    }
549
550    fn visit_seq<A: SeqAccess<'de>>(mut self, mut seq: A) -> Result<(), A::Error> {
551        let mut patterns = Vec::new();
552        while let Some(record) = seq.next_element::<ActionFile>()? {
553            let action = record.action.clone();
554            match self.declare(record) {
555                Ok(timed) => patterns.push((action, timed)),
556                Err(refusal) => return Err(self.refuse(refusal)),
557            }
558        }
559        for (action, timed) in patterns {
560            for (pattern, emits) in timed {
561                // Every check `attach` makes was made as the record was read;
562                // what it refuses anyway is reported, at the end of the list.
563                if let Err(error) = pattern.attach(self.map, &action, &emits) {
564                    return Err(self.refuse(AssetRefusal::Declare(error)));
565                }
566            }
567        }
568        Ok(())
569    }
570}
571
572// ---------------------------------------------------------------------------
573// Errors
574// ---------------------------------------------------------------------------
575
576/// Why a binding asset was refused, and where — see
577/// [`ActionMap::from_ron`].
578#[derive(Clone, Debug, PartialEq, Eq)]
579pub struct BindingAssetError {
580    line: usize,
581    column: usize,
582    refusal: AssetRefusal,
583}
584
585impl BindingAssetError {
586    /// The line ron was at when it stopped, 1-based.
587    #[must_use]
588    pub const fn line(&self) -> usize {
589        self.line
590    }
591
592    /// The column of [`line`](Self::line), 1-based as ron counts it.
593    #[must_use]
594    pub const fn column(&self) -> usize {
595        self.column
596    }
597
598    /// What was wrong.
599    #[must_use]
600    pub const fn refusal(&self) -> &AssetRefusal {
601        &self.refusal
602    }
603
604    fn at(error: &ron::error::SpannedError, refusal: AssetRefusal) -> Self {
605        Self {
606            line: error.span.start.line,
607            column: error.span.start.col,
608            refusal,
609        }
610    }
611
612    fn parse(error: ron::error::SpannedError) -> Self {
613        let message = error.code.to_string();
614        Self::at(&error, AssetRefusal::Parse(message))
615    }
616}
617
618impl fmt::Display for BindingAssetError {
619    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
620        write!(
621            f,
622            "line {}, column {}: {}",
623            self.line, self.column, self.refusal
624        )
625    }
626}
627
628impl std::error::Error for BindingAssetError {}
629
630/// What was wrong with a binding asset.
631#[derive(Clone, Debug, PartialEq, Eq)]
632pub enum AssetRefusal {
633    /// Not RON, or RON that is not the schema — an unknown field, an unknown
634    /// `kind`, a missing `action` — in ron's own words, which name it.
635    Parse(String),
636    /// A binding's text is not a [`Binding`].
637    BadBinding {
638        /// The record's action.
639        action: String,
640        /// Why the text did not parse, with the text.
641        error: BindingParseError,
642    },
643    /// A binding written in a device list that is not its device's — a key
644    /// under `gamepad`.
645    WrongList {
646        /// The record's action.
647        action: String,
648        /// The binding, as written.
649        binding: String,
650        /// The list it was written in; [`Binding::device`] names the right one.
651        list: Device,
652    },
653    /// Two taps, two holds or two double taps on one action.
654    DuplicatePattern {
655        /// The record's action.
656        action: String,
657        /// The pattern written twice.
658        pattern: Pattern,
659    },
660    /// A pattern time of zero milliseconds, which would fire on every press.
661    BadTime {
662        /// The record's action.
663        action: String,
664        /// The pattern.
665        pattern: Pattern,
666    },
667    /// A pattern that emits an action the file does not declare.
668    UnknownEmit {
669        /// The record's action.
670        action: String,
671        /// The pattern.
672        pattern: Pattern,
673        /// The name it emits.
674        emits: String,
675    },
676    /// A pattern that emits an action that is not a `Button`.
677    EmitNotAButton {
678        /// The record's action.
679        action: String,
680        /// The pattern.
681        pattern: Pattern,
682        /// The action it emits.
683        emits: String,
684    },
685    /// A pattern that emits the action it is on.
686    EmitsItself {
687        /// The record's action.
688        action: String,
689        /// The pattern.
690        pattern: Pattern,
691    },
692    /// The map refused the record: [`ActionMapError::DuplicateName`] for a
693    /// name declared twice, [`ActionMapError::InvalidDeadzone`] for a pad dead
694    /// zone or threshold outside `0.0..1.0`.
695    Declare(ActionMapError),
696}
697
698impl fmt::Display for AssetRefusal {
699    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700        match self {
701            Self::Parse(message) => f.write_str(message),
702            Self::BadBinding { action, error } => write!(f, "action {action:?}: {error}"),
703            Self::WrongList {
704                action,
705                binding,
706                list,
707            } => write!(
708                f,
709                "action {action:?}: {binding:?} is not a {} binding",
710                list_name(*list)
711            ),
712            Self::DuplicatePattern { action, pattern } => {
713                let pattern = pattern_name(*pattern);
714                write!(f, "action {action:?}: more than one {pattern}")
715            }
716            Self::BadTime { action, pattern } => {
717                let pattern = pattern_name(*pattern);
718                write!(f, "action {action:?}: a {pattern} of zero milliseconds")
719            }
720            Self::UnknownEmit {
721                action,
722                pattern,
723                emits,
724            } => {
725                let pattern = pattern_name(*pattern);
726                write!(
727                    f,
728                    "action {action:?}: {pattern} emits {emits:?}, which is not declared"
729                )
730            }
731            Self::EmitNotAButton {
732                action,
733                pattern,
734                emits,
735            } => {
736                let pattern = pattern_name(*pattern);
737                write!(
738                    f,
739                    "action {action:?}: {pattern} emits {emits:?}, which is not a Button"
740                )
741            }
742            Self::EmitsItself { action, pattern } => {
743                let pattern = pattern_name(*pattern);
744                write!(f, "action {action:?}: {pattern} emits its own action")
745            }
746            Self::Declare(error) => error.fmt(f),
747        }
748    }
749}
750
751/// Why [`ActionMap::to_ron`] cannot write a map: the schema has no way to say
752/// it, so the file would read back as a different map.
753#[derive(Clone, Debug, PartialEq, Eq)]
754pub enum AssetWriteError {
755    /// The action's defaults interleave devices — a pad button between two
756    /// keys — and the file's lists would read back in [`DEVICE_LISTS`] order.
757    MixedLists(String),
758    /// A pattern attached with no emitted action named; the schema's patterns
759    /// all emit one.
760    NoEmit {
761        /// The action.
762        action: String,
763        /// The pattern.
764        pattern: Pattern,
765    },
766    /// A pattern time that is not a whole number of milliseconds, or more
767    /// than a `u16` of them.
768    NotWholeMilliseconds {
769        /// The action.
770        action: String,
771        /// The pattern.
772        pattern: Pattern,
773    },
774    /// A repeat schedule ([`ActionMap::set_repeat`]), which the schema does not
775    /// have.
776    Repeat(String),
777}
778
779impl fmt::Display for AssetWriteError {
780    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
781        match self {
782            Self::MixedLists(action) => {
783                write!(f, "action {action:?}: its bindings interleave devices")
784            }
785            Self::NoEmit { action, pattern } => {
786                let pattern = pattern_name(*pattern);
787                write!(f, "action {action:?}: its {pattern} emits no action")
788            }
789            Self::NotWholeMilliseconds { action, pattern } => {
790                let pattern = pattern_name(*pattern);
791                write!(
792                    f,
793                    "action {action:?}: its {pattern} is not a whole number of milliseconds"
794                )
795            }
796            Self::Repeat(action) => write!(f, "action {action:?}: a repeat has no asset form"),
797        }
798    }
799}
800
801impl std::error::Error for AssetWriteError {}
802
803#[cfg(test)]
804mod tests;