Skip to main content

shuyi_platform/
script.rs

1//! Input scripts: the steps of a player's input a tool plays (keys,
2//! controller buttons, an action by name, waits in seconds, the crank, and
3//! the debug view's channels turned on) and the one plan that turns them into frames of raw input events
4//! ([`frames`]). The Harness plays a [`Script`] (`Script::play`, the
5//! `harness` feature), the live link's server plays a `Play` request's
6//! steps, and a capture plan's input steps are these (ticket 2566): a key
7//! press is written once.
8//!
9//! A step's form is the same in JSON and in RON: `{"keys": "Enter Up"}`,
10//! `{"pad": "South+East"}`, `{"act": "Confirm"}`, `{"wait": 0.5}`,
11//! `{"crank": 30}`, `{"debug": "grid"}`; `keys("Enter Up")` and so on in
12//! a plan's RON.
13//!
14//! Every frame is [`FRAME_DT`] long, and a wait is played as
15//! [`wait_frames`] of them. How long one wait may be is the player's to
16//! say, not the step's: `harness::MAX_WAIT`, `link::MAX_WAIT`, a capture
17//! plan's `MAX_WAIT`.
18
19use crate::alloc_prelude::*;
20use alloc::{format, vec};
21use core::fmt;
22
23use serde::{Deserialize, Serialize};
24use shuyi_ui::RawInputEvent;
25use shuyi_ui::input::{ActionSet, Button, ButtonChord, Chord, Keymap, PadKind};
26
27/// How long each frame of a script is: a 60th of a second.
28pub const FRAME_DT: f32 = 1.0 / 60.0;
29
30/// One step of a script, played in the order written.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33#[serde(rename_all = "kebab-case")]
34// A set that grows: a step is added without a break (ADR-0039).
35#[non_exhaustive]
36pub enum Step {
37    /// Each whitespace-separated key chord pressed in one frame and let go
38    /// in the next, in turn (`"Enter Up Shift+Space"`).
39    Keys(String),
40    /// Each whitespace-separated controller button pressed in one frame
41    /// and let go in the next, in turn (`"South"`; `"East+DpadDown"`:
42    /// those buttons together).
43    Pad(String),
44    /// The game's action of that name (`Confirm`), pressed by the player's
45    /// primary key for it, or its primary button if it has no key.
46    Act(String),
47    /// That many seconds of frames with no input, 60 a second, rounded up.
48    Wait(f32),
49    /// The crank wound that many degrees in one frame (backwards is
50    /// negative), as a Playdate front end reports it.
51    Crank(f32),
52    /// Debug tools and the debug view's channels of these comma-separated
53    /// ids turned on from here on (`"grid,collision"`; `"all"` turns on
54    /// every channel, offered or not), and the frame drawn again: no frame
55    /// passes (ADR-0085). Played by the Harness only.
56    Debug(String),
57}
58
59/// Whether `id` can name a debug view channel in a [`Step::Debug`]: not
60/// empty, and only `a`-`z`, `0`-`9` and `_`.
61fn is_channel_id(id: &str) -> bool {
62    !id.is_empty()
63        && id
64            .chars()
65            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
66}
67
68impl Step {
69    /// Whether it can be played: names `Chord::parse` and `Button::parse`
70    /// know, an action named, a wait that is a number of seconds, 0 or
71    /// more, and a crank's number of degrees. An action's name is looked
72    /// up when it plays, in the game's keymap; the longest wait is the
73    /// player's to check ([`check_waits`]).
74    pub fn check(&self) -> Result<(), String> {
75        match self {
76            Step::Keys(script) => {
77                for token in script.split_whitespace() {
78                    Chord::parse(token).map_err(|e| format!("keys: {e}"))?;
79                }
80            }
81            Step::Pad(script) => {
82                for token in script.split_whitespace() {
83                    buttons(token).map_err(|e| format!("pad: {e}"))?;
84                }
85            }
86            Step::Act(name) => {
87                if name.trim().is_empty() {
88                    return Err("act: no action named".to_owned());
89                }
90            }
91            Step::Wait(seconds) => {
92                if !(seconds.is_finite() && *seconds >= 0.0) {
93                    return Err(format!(
94                        "wait: {seconds} is not a number of seconds, 0 or more"
95                    ));
96                }
97            }
98            Step::Crank(degrees) => {
99                if !degrees.is_finite() {
100                    return Err(format!("crank: {degrees} is not a number of degrees"));
101                }
102            }
103            Step::Debug(channels) => {
104                if let Some(bad) = channels
105                    .split(',')
106                    .map(str::trim)
107                    .find(|id| !is_channel_id(id))
108                {
109                    return Err(format!(
110                        "debug: {bad:?} is not a channel's id (a-z, 0-9 and _, comma-separated)"
111                    ));
112                }
113            }
114        }
115        Ok(())
116    }
117}
118
119impl fmt::Display for Step {
120    /// The step as a plan's RON writes it: `keys("Enter")`, `wait(0.5)`.
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        match self {
123            Step::Keys(script) => write!(f, "keys({script:?})"),
124            Step::Pad(script) => write!(f, "pad({script:?})"),
125            Step::Act(name) => write!(f, "act({name:?})"),
126            Step::Wait(seconds) => write!(f, "wait({seconds})"),
127            Step::Crank(degrees) => write!(f, "crank({degrees})"),
128            Step::Debug(channels) => write!(f, "debug({channels:?})"),
129        }
130    }
131}
132
133/// Steps played in order.
134#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
135#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
136#[serde(deny_unknown_fields)]
137// Built with `Script::new` or `default()`: it may grow (ADR-0039).
138#[non_exhaustive]
139pub struct Script {
140    /// The steps, in order.
141    pub steps: Vec<Step>,
142}
143
144impl Script {
145    /// The script of `steps`, in order.
146    pub fn new(steps: Vec<Step>) -> Self {
147        Self { steps }
148    }
149
150    /// Whether every step can be played ([`Step::check`]); an error names
151    /// the step (`step 3: keys: …`).
152    pub fn check(&self) -> Result<(), String> {
153        for (i, step) in self.steps.iter().enumerate() {
154            step.check().map_err(|e| format!("step {}: {e}", i + 1))?;
155        }
156        Ok(())
157    }
158
159    /// Plays the steps in `h`, each frame [`FRAME_DT`] long: the frames of
160    /// [`frames`] with the game's keymap and the Harness's pad, and a crank
161    /// step with `Harness::crank`.
162    ///
163    /// # Panics
164    ///
165    /// On a step [`Step::check`] refuses, an action the game has no key or
166    /// button for, and a wait longer than `harness::MAX_WAIT`.
167    #[cfg(any(test, feature = "harness"))]
168    pub fn play<G: shuyi_ui::GameData>(&self, h: &mut crate::harness::Harness<G>) {
169        for (i, step) in self.steps.iter().enumerate() {
170            let pad = h.pad_kind();
171            let planned = match step {
172                Step::Crank(degrees) => step.check().map(|()| {
173                    h.crank(*degrees);
174                    Vec::new()
175                }),
176                Step::Debug(channels) => step.check().map(|()| {
177                    h.show_debug(channels);
178                    Vec::new()
179                }),
180                // Only an action needs the game: keys play while it loads.
181                Step::Act(_) => step_frames(step, h.game().input().keymap(), pad),
182                _ => plain_frames(step, pad),
183            };
184            let planned = planned.unwrap_or_else(|e| panic!("Harness: step {}: {e}", i + 1));
185            h.play_planned(&planned);
186        }
187    }
188}
189
190/// Whether every wait of `steps` is at most `max` seconds, the most the
191/// player plays in one step; an error names the step.
192pub fn check_waits(steps: &[Step], max: f32) -> Result<(), String> {
193    for (i, step) in steps.iter().enumerate() {
194        if let Step::Wait(seconds) = step
195            && *seconds > max
196        {
197            return Err(format!(
198                "step {}: wait: at most {max:.1} seconds in one step, not {seconds}",
199                i + 1
200            ));
201        }
202    }
203    Ok(())
204}
205
206/// How many frames a wait of `seconds` takes: the seconds at 60 a second,
207/// rounded up, a product within a thousandth of a whole number counting
208/// as that number (0.5 s is 30 frames, 1/60 s is 1, 0.51 s is 31). A wait
209/// above 0 is at least one frame; a wait of 0, or one that isn't a number
210/// of seconds 0 or more, is none.
211pub fn wait_frames(seconds: f32) -> u32 {
212    if !(seconds.is_finite() && seconds > 0.0) {
213        return 0;
214    }
215    // Thousandths of a frame, to the nearest: one over a whole number of
216    // frames rounds down, two round up. `as` saturates past `u64::MAX`.
217    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
218    let thousandths = (f64::from(seconds) * 60_000.0 + 0.5) as u64;
219    let frames = thousandths.saturating_add(998) / 1000;
220    u32::try_from(frames).unwrap_or(u32::MAX).max(1)
221}
222
223/// What presses an action: a key chord, or a button (or two together).
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225#[non_exhaustive]
226pub enum Press {
227    /// The action's primary key chord.
228    Keys(Chord),
229    /// Its primary button, or button chord.
230    Pad(ButtonChord),
231}
232
233/// What presses the game's action named `name` in `keymap`: its primary
234/// key ([`Keymap::primary`]), else its primary button
235/// ([`Keymap::primary_button`]). The one lookup of an action by name, so
236/// every tool presses an action as a player would. An error names the
237/// action: the game has none of that name, or it has neither a key nor a
238/// button.
239pub fn press_for<A: ActionSet>(keymap: &Keymap<A>, name: &str) -> Result<Press, String> {
240    let Some(action) = A::from_name(name) else {
241        let names: Vec<&str> = A::ALL.iter().map(|a| a.name()).collect();
242        return Err(format!(
243            "act: the game has no action {name:?}; its actions are {}",
244            names.join(", ")
245        ));
246    };
247    if let Some(chord) = keymap.primary(action) {
248        return Ok(Press::Keys(chord));
249    }
250    keymap
251        .primary_button(action)
252        .map(Press::Pad)
253        .ok_or_else(|| {
254            format!("act: {name} has neither a key nor a button in the player's keymap now")
255        })
256}
257
258/// The names of `keymap`'s actions that have a key or a button, in
259/// [`ActionSet::ALL`]'s order: those an [`Step::Act`] can press.
260pub fn pressable<A: ActionSet>(keymap: &Keymap<A>) -> Vec<String> {
261    A::ALL
262        .iter()
263        .filter(|&&a| keymap.primary(a).is_some() || keymap.primary_button(a).is_some())
264        .map(|a| a.name().to_owned())
265        .collect()
266}
267
268/// Frames of a script's plan.
269#[derive(Debug, Clone, PartialEq)]
270// What a plan gives: it may grow (ADR-0039).
271#[non_exhaustive]
272pub enum Planned {
273    /// One frame with these events, in order.
274    Events(Vec<RawInputEvent>),
275    /// That many frames with no input.
276    Idle(u32),
277}
278
279/// The frames `steps` give, each [`FRAME_DT`] long, with the game's
280/// `keymap` (for an action by name) and buttons pressed on a pad of kind
281/// `pad`: a key chord or a pad token down in one frame and up in the next,
282/// an action as its key or its button, a wait as [`wait_frames`] frames
283/// with no input (none for a wait of 0). An error names the step
284/// (`step 3: act: …`): one [`Step::check`] refuses, an action that can't be
285/// pressed, or a crank, which isn't played this way (the Harness winds it
286/// itself; the live link can't yet).
287pub fn frames<A: ActionSet>(
288    steps: &[Step],
289    keymap: &Keymap<A>,
290    pad: PadKind,
291) -> Result<Vec<Planned>, String> {
292    let mut out = Vec::new();
293    for (i, step) in steps.iter().enumerate() {
294        let planned = step_frames(step, keymap, pad).map_err(|e| format!("step {}: {e}", i + 1))?;
295        out.extend(planned);
296    }
297    Ok(out)
298}
299
300/// The frames of `step`, as [`frames`] gives them; an error doesn't name
301/// the step.
302fn step_frames<A: ActionSet>(
303    step: &Step,
304    keymap: &Keymap<A>,
305    pad: PadKind,
306) -> Result<Vec<Planned>, String> {
307    let Step::Act(name) = step else {
308        return plain_frames(step, pad);
309    };
310    step.check()?;
311    Ok(match press_for(keymap, name)? {
312        Press::Keys(chord) => key_press(chord).to_vec(),
313        Press::Pad(chord) => {
314            let buttons = match chord.buttons() {
315                Some((a, b)) => vec![a, b],
316                None => vec![chord.first()],
317            };
318            pad_press(&buttons, pad).to_vec()
319        }
320    })
321}
322
323/// The frames of `step`, a step that presses no action, as [`frames`]
324/// gives them.
325fn plain_frames(step: &Step, pad: PadKind) -> Result<Vec<Planned>, String> {
326    step.check()?;
327    Ok(match step {
328        Step::Keys(script) => key_frames(script)?,
329        Step::Pad(script) => pad_frames(script, pad)?,
330        Step::Wait(seconds) => match wait_frames(*seconds) {
331            0 => Vec::new(),
332            n => vec![Planned::Idle(n)],
333        },
334        Step::Act(_) => return Err("act: pressed with the game's keymap".to_owned()),
335        Step::Crank(_) => return Err("crank: not over the live link yet".to_owned()),
336        Step::Debug(_) => return Err("debug: not over the live link yet".to_owned()),
337    })
338}
339
340/// The frames of `script`'s key chords, each down in one frame and up in
341/// the next; an error is [`Chord::parse`]'s.
342pub(crate) fn key_frames(script: &str) -> Result<Vec<Planned>, String> {
343    let mut out = Vec::new();
344    for token in script.split_whitespace() {
345        out.extend(key_press(Chord::parse(token)?));
346    }
347    Ok(out)
348}
349
350/// The frames of `script`'s pad tokens on a pad of kind `pad`, each
351/// token's buttons down together in one frame and up in the next; an
352/// error is [`Button::parse`]'s.
353pub(crate) fn pad_frames(script: &str, pad: PadKind) -> Result<Vec<Planned>, String> {
354    let mut out = Vec::new();
355    for token in script.split_whitespace() {
356        out.extend(pad_press(&buttons(token)?, pad));
357    }
358    Ok(out)
359}
360
361/// The buttons of one pad token, `"South"` or `"South+East"`.
362fn buttons(token: &str) -> Result<Vec<Button>, String> {
363    token.split('+').map(Button::parse).collect()
364}
365
366/// `chord` down in one frame and its key up in the next.
367fn key_press(chord: Chord) -> [Planned; 2] {
368    [
369        Planned::Events(vec![RawInputEvent::Down(chord)]),
370        Planned::Events(vec![RawInputEvent::Up(chord.key)]),
371    ]
372}
373
374/// `buttons` down together on a pad of kind `pad` in one frame, in the
375/// order given, and up in the next.
376fn pad_press(buttons: &[Button], pad: PadKind) -> [Planned; 2] {
377    [
378        Planned::Events(
379            buttons
380                .iter()
381                .map(|&b| RawInputEvent::PadDown(b, pad))
382                .collect(),
383        ),
384        Planned::Events(buttons.iter().map(|&b| RawInputEvent::PadUp(b)).collect()),
385    ]
386}
387
388#[cfg(test)]
389mod tests;