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;