Skip to main content

gasm_host/
script.rs

1//! Scripted input for headless runs (`--input`), mirrored by runners/web/headless.mjs.
2//!
3//! Items are `FRAMES:ACTION`, comma-separated (commas inside quotes or parentheses
4//! don't split). FRAMES is `N` or `FROM-TO` (inclusive). Actions:
5//!
6//! - `A+B+START` virtual pad 1 buttons
7//! - `"text"` typed text on frame N (escapes `\n` enter, `\b` backspace, `\\`, `\"`)
8//! - `KEY(ShiftLeft+ArrowLeft)` raw keys held (W3C names); press/release events
9//!   come from the changes between frames
10//! - `PTR(x,y)` / `PTR(x,y,L+R)` pointer position in drawable pixels and buttons
11//!   (`L R M BACK FWD`); the position stays where the last item put it
12//! - `MOVE(dx,dy)` relative motion, `WHEEL(x,y)` wheel, per frame
13//! - `GP0(B0+B9+A1=0.5)` gamepad slot 0-3: buttons by index, axes `An=value`;
14//!   slots that appear anywhere in the script are connected (standard mapping)
15//!
16//! All arithmetic is done in f64 and rounded to f32 once, exactly like the JS runner.
17
18use crate::host::{Gamepad, KEY_STATE_BYTES, Pointer, RawInput};
19use crate::keymap::KEY_CODES;
20
21const PAD_NAMES: [&str; 12] = ["A", "B", "X", "Y", "L", "R", "SELECT", "START", "UP", "DOWN", "LEFT", "RIGHT"];
22const MOUSE_NAMES: [&str; 5] = ["L", "R", "M", "BACK", "FWD"];
23
24#[derive(Default, Clone)]
25pub struct Script {
26    pads: Vec<(u64, u64, u32)>,
27    text: Vec<(u64, String)>,
28    keys: Vec<(u64, u64, Vec<u16>)>,
29    ptr: Vec<(u64, u64, f64, f64, u32)>,
30    moves: Vec<(u64, u64, f64, f64)>,
31    wheel: Vec<(u64, u64, f64, f64)>,
32    /// (from, to, slot, buttons [(index, value)], axes [(index, value)])
33    gp: Vec<(u64, u64, usize, Vec<(usize, f64)>, Vec<(usize, f64)>)>,
34}
35
36/// Carried between frames: previous keys, pointer position and buttons.
37#[derive(Default)]
38pub struct ScriptState {
39    keys: [u8; KEY_STATE_BYTES],
40    x: f64,
41    y: f64,
42    buttons: u32,
43}
44
45fn split_items(spec: &str) -> Vec<String> {
46    let (mut items, mut cur, mut quoted, mut escaped, mut depth) = (Vec::new(), String::new(), false, false, 0);
47    for ch in spec.chars() {
48        if escaped {
49            escaped = false;
50        } else if ch == '\\' && quoted {
51            escaped = true;
52        } else if ch == '"' {
53            quoted = !quoted;
54        } else if !quoted && ch == '(' {
55            depth += 1;
56        } else if !quoted && ch == ')' {
57            depth -= 1;
58        } else if ch == ',' && !quoted && depth == 0 {
59            items.push(std::mem::take(&mut cur));
60            continue;
61        }
62        cur.push(ch);
63    }
64    items.push(cur);
65    items
66}
67
68fn args<'a>(rest: &'a str, name: &str) -> Option<&'a str> {
69    rest.strip_prefix(name)?.strip_prefix('(')?.strip_suffix(')')
70}
71
72impl Script {
73    pub fn parse(spec: &str) -> Result<Script, String> {
74        let mut s = Script::default();
75        for item in split_items(spec) {
76            let bad = || format!("bad --input item {item:?}");
77            let (range, rest) = item.split_once(':').ok_or_else(bad)?;
78            let (from, to) = range.split_once('-').unwrap_or((range, range));
79            let (from, to): (u64, u64) = (from.parse().map_err(|_| bad())?, to.parse().map_err(|_| bad())?);
80            let num = |v: &str| v.trim().parse::<f64>().map_err(|_| bad());
81            let pair = |a: &str| -> Result<(f64, f64), String> {
82                let (x, y) = a.split_once(',').ok_or_else(bad)?;
83                Ok((num(x)?, num(y)?))
84            };
85            if let Some(q) = rest.strip_prefix('"') {
86                let body = q.strip_suffix('"').ok_or_else(bad)?;
87                let mut t = String::new();
88                let mut chars = body.chars();
89                while let Some(c) = chars.next() {
90                    t.push(if c != '\\' {
91                        c
92                    } else {
93                        match chars.next().ok_or_else(bad)? {
94                            'n' => '\n',
95                            'b' => '\u{8}',
96                            other => other,
97                        }
98                    });
99                }
100                s.text.push((from, t));
101            } else if let Some(a) = args(rest, "KEY") {
102                let mut codes = Vec::new();
103                for k in a.split('+') {
104                    let c = KEY_CODES.iter().position(|n| !n.is_empty() && *n == k).ok_or_else(|| format!("{}: unknown key {k:?}", bad()))?;
105                    codes.push(c as u16);
106                }
107                s.keys.push((from, to, codes));
108            } else if let Some(a) = args(rest, "PTR") {
109                let parts: Vec<&str> = a.splitn(3, ',').collect();
110                if parts.len() < 2 {
111                    return Err(bad());
112                }
113                let mut buttons = 0;
114                if let Some(b) = parts.get(2) {
115                    for n in b.split('+') {
116                        buttons |= 1 << MOUSE_NAMES.iter().position(|m| m.eq_ignore_ascii_case(n.trim())).ok_or_else(bad)?;
117                    }
118                }
119                s.ptr.push((from, to, num(parts[0])?, num(parts[1])?, buttons));
120            } else if let Some(a) = args(rest, "MOVE") {
121                let (x, y) = pair(a)?;
122                s.moves.push((from, to, x, y));
123            } else if let Some(a) = args(rest, "WHEEL") {
124                let (x, y) = pair(a)?;
125                s.wheel.push((from, to, x, y));
126            } else if rest.starts_with("GP") && rest.len() > 3 {
127                let slot: usize = rest[2..3].parse().map_err(|_| bad())?;
128                let a = args(&rest[3..], "").ok_or_else(bad)?;
129                if slot > 3 {
130                    return Err(bad());
131                }
132                let (mut buttons, mut axes) = (Vec::new(), Vec::new());
133                for part in a.split('+').filter(|p| !p.is_empty()) {
134                    if let Some(i) = part.strip_prefix('B') {
135                        buttons.push((i.parse().map_err(|_| bad())?, 1.0));
136                    } else if let Some(ax) = part.strip_prefix('A') {
137                        let (i, v) = ax.split_once('=').ok_or_else(bad)?;
138                        axes.push((i.parse().map_err(|_| bad())?, num(v)?));
139                    } else {
140                        return Err(bad());
141                    }
142                }
143                s.gp.push((from, to, slot, buttons, axes));
144            } else {
145                let mut mask = 0;
146                for b in rest.split('+') {
147                    mask |= 1 << PAD_NAMES.iter().position(|n| n.eq_ignore_ascii_case(b)).ok_or_else(bad)?;
148                }
149                s.pads.push((from, to, mask));
150            }
151        }
152        Ok(s)
153    }
154
155    pub fn pad(&self, frame: u64) -> u32 {
156        self.pads.iter().filter(|(f, t, _)| (*f..=*t).contains(&frame)).fold(0, |m, (_, _, b)| m | b)
157    }
158
159    pub fn text(&self, frame: u64) -> String {
160        self.text.iter().filter(|(f, _)| *f == frame).map(|(_, t)| t.as_str()).collect()
161    }
162
163    /// Raw input for `frame`. `drawable`: the headless drawable size; `mode`: input_mode flags.
164    pub fn raw(&self, frame: u64, st: &mut ScriptState, drawable: (f32, f32), mode: u32) -> RawInput {
165        let on = |f: u64, t: u64| (f..=t).contains(&frame);
166        let mut keys = [0u8; KEY_STATE_BYTES];
167        for (f, t, codes) in &self.keys {
168            if on(*f, *t) {
169                for &c in codes {
170                    keys[c as usize / 8] |= 1 << (c % 8);
171                }
172            }
173        }
174        // releases first, then presses, each in code order
175        let mut key_events = Vec::new();
176        for down in [false, true] {
177            for c in 1..KEY_CODES.len() as u16 {
178                let (was, is) = (st.keys[c as usize / 8] >> (c % 8) & 1 == 1, keys[c as usize / 8] >> (c % 8) & 1 == 1);
179                if was != is && is == down {
180                    key_events.push((c, down));
181                }
182            }
183        }
184        st.keys = keys;
185
186        let (px, py) = (st.x, st.y);
187        let mut buttons = 0;
188        for (f, t, x, y, b) in &self.ptr {
189            if on(*f, *t) {
190                st.x = *x;
191                st.y = *y;
192                buttons = *b;
193            }
194        }
195        let (mut dx, mut dy) = (st.x - px, st.y - py);
196        for (f, t, x, y) in &self.moves {
197            if on(*f, *t) {
198                dx += x;
199                dy += y;
200            }
201        }
202        let (mut wx, mut wy) = (0.0, 0.0);
203        for (f, t, x, y) in &self.wheel {
204            if on(*f, *t) {
205                wx += x;
206                wy += y;
207            }
208        }
209        let inside = st.x >= 0.0 && st.y >= 0.0 && st.x < drawable.0 as f64 && st.y < drawable.1 as f64;
210        let pointer = Pointer {
211            x: st.x as f32,
212            y: st.y as f32,
213            dx: dx as f32,
214            dy: dy as f32,
215            wheel_x: wx as f32,
216            wheel_y: wy as f32,
217            buttons,
218            pressed: buttons & !st.buttons,
219            released: st.buttons & !buttons,
220            flags: (inside as u32) | (mode & 6),
221            drawable,
222        };
223        st.buttons = buttons;
224
225        let mut gamepads: [Gamepad; 4] = Default::default();
226        for (slot, g) in gamepads.iter_mut().enumerate() {
227            if self.gp.iter().any(|e| e.2 == slot) {
228                *g = Gamepad { connected: true, standard: true, buttons: vec![0.0; 17], axes: vec![0.0; 4], name: "scripted".into() };
229            }
230        }
231        for (f, t, slot, bs, axs) in &self.gp {
232            if on(*f, *t) {
233                let g = &mut gamepads[*slot];
234                for &(i, v) in bs {
235                    if i < 32 {
236                        g.buttons.resize(g.buttons.len().max(i + 1), 0.0);
237                        g.buttons[i] = v as f32;
238                    }
239                }
240                for &(i, v) in axs {
241                    if i < 16 {
242                        g.axes.resize(g.axes.len().max(i + 1), 0.0);
243                        g.axes[i] = v as f32;
244                    }
245                }
246            }
247        }
248        RawInput { keys: Some(keys), key_events, pointer: Some(pointer), gamepads: Some(gamepads) }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn parses_all_actions_and_derives_events() {
258        let s = Script::parse(r#"0-1:A+UP,2:"x,y",3-4:KEY(ShiftLeft+ArrowLeft),5:PTR(10,20,L),6:PTR(15,20),6:WHEEL(0,1),7-8:GP1(B0+A1=0.5),9:MOVE(3,-4)"#).unwrap();
259        assert_eq!(s.pad(1), 1 | 1 << 8);
260        assert_eq!(s.text(2), "x,y");
261        let mut st = ScriptState::default();
262        let d = (1280.0, 720.0);
263        let mut frames: Vec<RawInput> = (0..10).map(|i| s.raw(i, &mut st, d, 0)).collect();
264        let shift = KEY_CODES.iter().position(|n| *n == "ShiftLeft").unwrap() as u16;
265        assert!(frames[3].key_events.contains(&(shift, true)));
266        assert!(frames[5].key_events.contains(&(shift, false)));
267        let p5 = frames[5].pointer.unwrap();
268        assert_eq!((p5.x, p5.y, p5.buttons, p5.pressed), (10.0, 20.0, 1, 1));
269        let p6 = frames[6].pointer.unwrap();
270        assert_eq!((p6.dx, p6.released, p6.wheel_y), (5.0, 1, 1.0));
271        let g = frames.remove(7).gamepads.unwrap();
272        assert!(g[1].connected && !g[0].connected);
273        assert_eq!((g[1].buttons[0], g[1].axes[1]), (1.0, 0.5));
274        assert_eq!(frames[8].pointer.unwrap().dx, 3.0); // frames shifted by remove(7): index 8 = frame 9
275        assert!(Script::parse("1:KEY(NoSuchKey)").is_err());
276    }
277}