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