Skip to main content

badterm_core/
igs.rs

1//! Atari ST BBSes with IGS, Larry Mears's "Instant Graphics and Sound"
2//! (version 2.19): VT-52 text and GEM VDI drawing on the ST's own screen,
3//! 640x200 in four colors (medium resolution, where BBSes start), 320x200
4//! in sixteen or 640x400 in two.
5//!
6//! `G#` gets IGS's attention; then a command letter, a `>` to chain
7//! another command after it (anything else ends the chain), and numbers,
8//! each ended by any one character that isn't a digit. The parsing follows
9//! IG's own source (IG217.C, and IG 2.19's notes): numbers of up to four
10//! digits, `_` and line breaks inside a number skipped, `x`, `y`, `r`, `R`,
11//! `+n`, `-n` and `!n` for loops and random values.
12//!
13//! Everything, text included, is drawn into a bitmap of color registers,
14//! which the front end shows like RIPscrip's graphics. Fill patterns, hatches,
15//! line styles and marker shapes are the VDI's, as EmuTOS (GPL 2 or later)
16//! defines them. Sound (the sound chip's effects, chip music and MIDI) is
17//! read past and not played.
18
19use std::time::{Duration, Instant};
20
21use crate::palette::Rgb;
22
23static FONT8: &[u8; 2048] = include_bytes!("../assets/vga_8x8.bin");
24static FONT16: &[u8; 4096] = include_bytes!("../assets/vga_8x16.bin");
25/// BadTerm's own 6x6 font (see assets/make_font6.py), for 8-point text.
26static FONT6: &[u8; 1536] = include_bytes!("../assets/font6x6.bin");
27
28pub const MAX_WIDTH: usize = 640;
29pub const MAX_HEIGHT: usize = 400;
30/// The text screen the front end lays out (the bitmap is stretched over it).
31pub const COLS: usize = 80;
32pub const ROWS: usize = 25;
33/// What `G#?>0:` answers.
34const VERSION: &[u8] = b"2.19";
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum Res {
38    Low,
39    Medium,
40    High,
41}
42
43impl Res {
44    pub fn size(self) -> (usize, usize) {
45        match self {
46            Res::Low => (320, 200),
47            Res::Medium => (640, 200),
48            Res::High => (640, 400),
49        }
50    }
51
52    fn colors(self) -> u8 {
53        match self {
54            Res::Low => 16,
55            Res::Medium => 4,
56            Res::High => 2,
57        }
58    }
59
60    /// The text cell: 8x8, or 8x16 in high resolution.
61    fn cell_h(self) -> usize {
62        if self == Res::High { 16 } else { 8 }
63    }
64
65    /// The color register a VDI pen draws in.
66    fn register(self, pen: u8) -> u8 {
67        const LOW: [u8; 16] = [0, 15, 1, 2, 4, 6, 3, 5, 7, 8, 9, 10, 12, 14, 11, 13];
68        match self {
69            Res::Low => LOW[(pen & 15) as usize],
70            Res::Medium => [0, 3, 1, 2].get(pen as usize).copied().unwrap_or(3),
71            Res::High => (pen != 0) as u8,
72        }
73    }
74
75    /// A circle's height for its width: the screen's pixels aren't square.
76    fn aspect(self) -> f64 {
77        match self {
78            Res::Low => 338.0 / 372.0,
79            Res::Medium => 169.0 / 372.0,
80            Res::High => 1.0,
81        }
82    }
83
84    fn number(self) -> u8 {
85        match self {
86            Res::Low => 0,
87            Res::Medium => 1,
88            Res::High => 2,
89        }
90    }
91}
92
93/// The ST's colors are 3 bits a channel: 0x0RGB.
94type StColor = u16;
95
96/// The desktop's (TOS's) colors, by register.
97const DESKTOP: [StColor; 16] =
98    [0x777, 0x700, 0x070, 0x770, 0x007, 0x707, 0x077, 0x555, 0x333, 0x733, 0x373, 0x773, 0x337, 0x737, 0x377, 0x000];
99const DESKTOP_MEDIUM: [StColor; 4] = [0x777, 0x700, 0x070, 0x000];
100/// IG 2.18's own palette, by register.
101const IG_PALETTE: [StColor; 16] =
102    [0x777, 0x700, 0x070, 0x000, 0x007, 0x770, 0x077, 0x546, 0x532, 0x743, 0x043, 0x030, 0x333, 0x210, 0x134, 0x000];
103
104fn rgb(c: StColor) -> Rgb {
105    let v = |n: u16| ((n & 7) as u32 * 255 + 3) / 7;
106    v(c >> 8) << 16 | v(c >> 4) << 8 | v(c)
107}
108
109// The VDI's patterns (from EmuTOS's vdi_fill.c and vdi_line.c).
110const ROM_UD_PATTERN: [u16; 16] = [
111    0x07E0, 0x0FF0, 0x1FD8, 0x1808, 0x1808, 0x1008, 0x1E78, 0x1348, 0x1108, 0x0810, 0x0B70, 0x0650, 0x07A0, 0x1E20,
112    0x1BC0, 0x1800,
113];
114const DITHER: [u16; 32] = [
115    0x0000, 0x4444, 0x0000, 0x1111, 0x0000, 0x5555, 0x0000, 0x5555, 0x8888, 0x5555, 0x2222, 0x5555, 0xAAAA, 0x5555,
116    0xAAAA, 0x5555, 0xAAAA, 0xDDDD, 0xAAAA, 0x7777, 0xAAAA, 0xFFFF, 0xAAAA, 0xFFFF, 0xEEEE, 0xFFFF, 0xBBBB, 0xFFFF,
117    0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
118];
119const OEMPAT: [u16; 128] = [
120    0xFFFF, 0x8080, 0x8080, 0x8080, 0xFFFF, 0x0808, 0x0808, 0x0808, 0x2020, 0x4040, 0x8080, 0x4141, 0x2222, 0x1414,
121    0x0808, 0x1010, 0x0000, 0x0000, 0x1010, 0x2828, 0x0000, 0x0000, 0x0101, 0x8282, 0x0202, 0x0202, 0xAAAA, 0x5050,
122    0x2020, 0x2020, 0xAAAA, 0x0505, 0x4040, 0x8080, 0x0000, 0x0808, 0x0404, 0x0202, 0x0000, 0x2020, 0x6606, 0xC6C6,
123    0xD8D8, 0x1818, 0x8181, 0x8DB1, 0x0C33, 0x6000, 0x0000, 0x0000, 0x0400, 0x0000, 0x0010, 0x0000, 0x8000, 0x0000,
124    0xF8F8, 0x6C6C, 0xC6C6, 0x8F8F, 0x1F1F, 0x3636, 0x6363, 0xF1F1, 0xAAAA, 0x0000, 0x8888, 0x1414, 0x2222, 0x4141,
125    0x8888, 0x0000, 0x0808, 0x0000, 0xAAAA, 0x0000, 0x0808, 0x0000, 0x8888, 0x0000, 0x7777, 0x9898, 0xF8F8, 0xF8F8,
126    0x7777, 0x8989, 0x8F8F, 0x8F8F, 0x8080, 0x8080, 0x4141, 0x3E3E, 0x0808, 0x0808, 0x1414, 0xE3E3, 0x8181, 0x4242,
127    0x2424, 0x1818, 0x0606, 0x0101, 0x8080, 0x8080, 0xF0F0, 0xF0F0, 0xF0F0, 0xF0F0, 0x0F0F, 0x0F0F, 0x0F0F, 0x0F0F,
128    0x0808, 0x1C1C, 0x3E3E, 0x7F7F, 0xFFFF, 0x7F7F, 0x3E3E, 0x1C1C, 0x1111, 0x2222, 0x4444, 0xFFFF, 0x8888, 0x4444,
129    0x2222, 0xFFFF,
130];
131const HATCH0: [u16; 48] = [
132    0x0101, 0x0202, 0x0404, 0x0808, 0x1010, 0x2020, 0x4040, 0x8080, 0x6060, 0xC0C0, 0x8181, 0x0303, 0x0606, 0x0C0C,
133    0x1818, 0x3030, 0x4242, 0x8181, 0x8181, 0x4242, 0x2424, 0x1818, 0x1818, 0x2424, 0x8080, 0x8080, 0x8080, 0x8080,
134    0x8080, 0x8080, 0x8080, 0x8080, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0x8080,
135    0x8080, 0x8080, 0x8080, 0x8080, 0x8080, 0x8080,
136];
137const HATCH1: [u16; 96] = [
138    0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080, 0x0100, 0x0200, 0x0400, 0x0800, 0x1000, 0x2000,
139    0x4000, 0x8000, 0x8003, 0x0007, 0x000E, 0x001C, 0x0038, 0x0070, 0x00E0, 0x01C0, 0x0380, 0x0700, 0x0E00, 0x1C00,
140    0x3800, 0x7000, 0xE000, 0xC001, 0x8001, 0x4002, 0x2004, 0x1008, 0x0810, 0x0420, 0x0240, 0x0180, 0x0180, 0x0240,
141    0x0420, 0x0810, 0x1008, 0x2004, 0x4002, 0x8001, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000,
142    0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0x8000, 0xFFFF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
143    0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF, 0x8080, 0x8080, 0x8080,
144    0x8080, 0x8080, 0x8080, 0x8080, 0xFFFF, 0x8080, 0x8080, 0x8080, 0x8080, 0x8080, 0x8080, 0x8080,
145];
146const LINE_STYLES: [u16; 6] = [0xFFFF, 0xFFF0, 0xC0C0, 0xFF18, 0xFF00, 0xF191];
147/// Polymarkers (EmuTOS's vdi_marker.c): polylines of points, in units of
148/// a 7-pixel marker.
149const MARKERS: [&[&[(i32, i32)]]; 6] = [
150    &[&[(0, 0), (0, 0)]],
151    &[&[(0, -3), (0, 3)], &[(-4, 0), (4, 0)]],
152    &[&[(0, -3), (0, 3)], &[(3, 2), (-3, -2)], &[(3, -2), (-3, 2)]],
153    &[&[(-4, -3), (4, -3), (4, 3), (-4, 3), (-4, -3)]],
154    &[&[(-4, -3), (4, 3)], &[(-4, 3), (4, -3)]],
155    &[&[(-4, 0), (0, -3), (4, 0), (0, 3), (-4, 0)]],
156];
157
158/// A number as IG reads it, before a loop gives the letters their values.
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160enum Tok {
161    Num(i32),
162    /// `x`: the loop's value.
163    X,
164    /// `y`: the loop's value counted the other way.
165    Y,
166    /// `r` and `R`: random, in their ranges.
167    Rand,
168    RandBig,
169    /// `+n`, `-n`, `!n`: the loop's value plus n, minus n, n minus it.
170    Plus(i32),
171    Minus(i32),
172    Bang(i32),
173}
174
175/// The bytes ran out before the command did: wait for more.
176#[derive(Debug)]
177struct Incomplete;
178
179type Res2<T> = Result<T, Incomplete>;
180
181struct Reader<'a> {
182    data: &'a [u8],
183    pos: usize,
184}
185
186impl Reader<'_> {
187    fn next(&mut self) -> Res2<u8> {
188        let b = *self.data.get(self.pos).ok_or(Incomplete)?;
189        self.pos += 1;
190        Ok(b)
191    }
192
193    /// A number (see the module's notes).
194    fn token(&mut self) -> Res2<Tok> {
195        let (mut num, mut digits) = (0i32, 0);
196        let mut sign: Option<u8> = None;
197        loop {
198            match self.next()? {
199                d @ b'0'..=b'9' => {
200                    num = num * 10 + (d - b'0') as i32;
201                    digits += 1;
202                }
203                // Line breaks may split a number (IG 2.18), and `_` marks
204                // where one does.
205                b'\r' | b'\n' | b'_' => {}
206                c @ (b'x' | b'y' | b'r' | b'R') if digits == 0 => {
207                    self.next()?;
208                    return Ok(match c {
209                        b'x' => Tok::X,
210                        b'y' => Tok::Y,
211                        b'r' => Tok::Rand,
212                        _ => Tok::RandBig,
213                    });
214                }
215                c @ (b'+' | b'-' | b'!') if digits == 0 && sign.is_none() => sign = Some(c),
216                _ => break,
217            }
218        }
219        if digits > 4 {
220            num = 0;
221        }
222        Ok(match sign {
223            Some(b'+') => Tok::Plus(num),
224            Some(b'-') => Tok::Minus(num),
225            Some(_) => Tok::Bang(num),
226            None => Tok::Num(num),
227        })
228    }
229
230    /// Text up to `@` (line breaks dropped), at most 128 characters.
231    fn text(&mut self) -> Res2<Vec<u8>> {
232        let mut out = Vec::new();
233        loop {
234            match self.next()? {
235                b'@' => return Ok(out),
236                b'\r' | b'\n' => {}
237                b if out.len() < 128 => out.push(b),
238                _ => return Ok(out),
239            }
240        }
241    }
242}
243
244/// A running `&` loop: IG 2.17's value() for looping, ported as it is.
245struct Loop {
246    params: Vec<Tok>,
247    at: usize,
248    /// lp1 (the value), lp2 (to), lp3 (step), lp5 (from).
249    value: i32,
250    to: i32,
251    step: i32,
252    from: i32,
253    /// `|` (XOR stepping) or `@` (W reads its text each time).
254    kind: u8,
255    x_count: i32,
256    first: i32,
257    second: i32,
258    was: i32,
259    /// No `x`-like parameters: the value steps once a pass.
260    plain: bool,
261    running: bool,
262}
263
264impl Loop {
265    fn next(&mut self) -> Tok {
266        if self.at == 0 {
267            self.x_count = 0;
268        }
269        let t = self.params[self.at];
270        let relative = !matches!(t, Tok::Num(_) | Tok::Rand | Tok::RandBig);
271        let mut out = t;
272        if relative {
273            self.x_count += 1;
274            if self.second == 0 {
275                self.was = self.value;
276            }
277            if self.x_count < 2 && self.first != 0 {
278                self.value += self.step;
279                self.second += 1;
280                if self.second == 1 && self.kind == b'|' {
281                    self.value = self.was;
282                }
283                if self.second == 2 && self.kind == b'|' {
284                    self.second = 0;
285                }
286            }
287            self.first += 1;
288            out = Tok::Num(match t {
289                Tok::X => self.value,
290                Tok::Y => match self.from.cmp(&self.to) {
291                    std::cmp::Ordering::Less => self.to - self.value,
292                    std::cmp::Ordering::Greater => self.from - self.value,
293                    std::cmp::Ordering::Equal => 0,
294                },
295                Tok::Plus(n) => n + self.value,
296                Tok::Minus(n) => self.value - n,
297                Tok::Bang(n) => n - self.value,
298                t => return t,
299            });
300        }
301        if self.plain && self.at == 0 {
302            self.value += self.step;
303        }
304        let last = self.at == self.params.len() - 1;
305        if last && (self.step >= 1 && self.value >= self.to || self.step < 0 && self.value <= self.to) {
306            self.running = false;
307        }
308        self.at = (self.at + 1) % self.params.len();
309        out
310    }
311}
312
313#[derive(Clone, Copy)]
314struct LineAttr {
315    pen: u8,
316    style: u8,
317    width: i32,
318    /// 0 square, 1 arrow, 2 round: (start, end).
319    ends: (u8, u8),
320}
321
322#[derive(Clone, Copy)]
323struct FillAttr {
324    pen: u8,
325    /// 0 hollow, 1 solid, 2 pattern, 3 hatch, 4 user.
326    interior: u8,
327    style: u8,
328    perimeter: bool,
329}
330
331#[derive(Clone, Copy)]
332struct TextAttr {
333    pen: u8,
334    effects: u8,
335    points: i32,
336    /// Quarter turns.
337    rotation: u8,
338}
339
340#[derive(Clone, Copy)]
341struct Attrs {
342    line: LineAttr,
343    fill: FillAttr,
344    text: TextAttr,
345    marker: (u8, u8, i32),
346    /// 1 replace, 2 transparent, 3 XOR, 4 reverse transparent.
347    mode: u8,
348}
349
350impl Default for Attrs {
351    fn default() -> Attrs {
352        Attrs {
353            line: LineAttr { pen: 1, style: 1, width: 1, ends: (0, 0) },
354            fill: FillAttr { pen: 1, interior: 1, style: 1, perimeter: true },
355            text: TextAttr { pen: 1, effects: 0, points: 9, rotation: 0 },
356            marker: (1, 1, 1),
357            mode: 1,
358        }
359    }
360}
361
362/// A running `X 8` color rotation.
363#[derive(Clone, Copy, Debug)]
364struct Cycle {
365    from: u8,
366    to: u8,
367    steps: u32,
368    step: Duration,
369}
370
371/// A mouse zone: its rectangle (x0, y0, x1, y1) and what it sends.
372type Zone = (i32, i32, i32, i32, Vec<u8>);
373
374/// What the VT-52 escape parser is waiting for.
375#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376enum Esc {
377    None,
378    Start,
379    Row,
380    Col(u8),
381    Fg,
382    Bg,
383}
384
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386enum Mode {
387    Text,
388    /// After `G#`, or a `>`: a command letter comes next.
389    Command,
390}
391
392pub struct Igs {
393    pub res: Res,
394    pixels: Vec<u8>,
395    /// The blit buffer, screen sized.
396    memory: Vec<u8>,
397    memory_rect: (i32, i32, i32, i32),
398    regs: [StColor; 16],
399    attrs: Attrs,
400    user_patterns: [[u16; 16]; 8],
401    /// Where `D` draws from.
402    pen_at: (i32, i32),
403    hollow: bool,
404    scaling: i32,
405    rand: (i32, i32),
406    rand_big: (i32, i32),
407    seed: u32,
408    // VT-52.
409    col: usize,
410    row: usize,
411    fg: u8,
412    bg: u8,
413    reverse: bool,
414    wrap: bool,
415    pub cursor_on: bool,
416    destructive_bs: bool,
417    saved: (usize, usize),
418    esc: Esc,
419    // Parsing.
420    mode: Mode,
421    /// A `G` that may start `G#`.
422    g: bool,
423    buf: Vec<u8>,
424    /// IG commands stored by `X 9`, and when they're to run.
425    stored: Vec<u8>,
426    run_stored: Option<Vec<u8>>,
427    /// `t` and `q`: read nothing more until then.
428    pub hold: Option<Instant>,
429    /// `X 8`: pens' colors rotating, a step each hold.
430    cycle: Option<Cycle>,
431    /// The palette `X 8,1,1,1,1` keeps.
432    kept_palette: [StColor; 16],
433    looping: Option<Loop>,
434    last_text: Vec<u8>,
435    // The mouse.
436    zones: Vec<Option<Zone>>,
437    /// `<` with a mouse zone: (send a CR after, transmit it).
438    awaiting_zone: Option<(bool, bool)>,
439    /// What the BBS should get (replies, zones clicked).
440    pub replies: Vec<u8>,
441    pub bells: usize,
442}
443
444impl Default for Igs {
445    fn default() -> Igs {
446        let mut igs = Igs {
447            res: Res::Medium,
448            pixels: Vec::new(),
449            memory: Vec::new(),
450            memory_rect: (0, 0, 0, 0),
451            regs: DESKTOP,
452            attrs: Attrs::default(),
453            user_patterns: [ROM_UD_PATTERN; 8],
454            pen_at: (0, 0),
455            hollow: false,
456            scaling: 0,
457            rand: (0, 199),
458            rand_big: (0, 199),
459            seed: 0x1234_5678,
460            col: 0,
461            row: 0,
462            fg: 3,
463            bg: 0,
464            reverse: false,
465            wrap: true,
466            cursor_on: true,
467            destructive_bs: false,
468            saved: (0, 0),
469            esc: Esc::None,
470            mode: Mode::Text,
471            g: false,
472            buf: Vec::new(),
473            stored: Vec::new(),
474            run_stored: None,
475            hold: None,
476            cycle: None,
477            kept_palette: DESKTOP,
478            looping: None,
479            last_text: Vec::new(),
480            zones: vec![None; 48],
481            awaiting_zone: None,
482            replies: Vec::new(),
483            bells: 0,
484        };
485        igs.set_res(Res::Medium);
486        igs.desktop_palette();
487        igs
488    }
489}
490
491impl Igs {
492    pub fn width(&self) -> usize {
493        self.res.size().0
494    }
495
496    pub fn height(&self) -> usize {
497        self.res.size().1
498    }
499
500    /// The bitmap: a color register a pixel.
501    pub fn pixels(&self) -> &[u8] {
502        &self.pixels
503    }
504
505    pub fn palette(&self) -> [Rgb; 16] {
506        std::array::from_fn(|i| rgb(self.regs[i]))
507    }
508
509    /// Where the VT-52 cursor is, on the front end's 80x25 text screen.
510    pub fn cursor(&self) -> (usize, usize) {
511        match self.res {
512            Res::Low => (self.col * 2, self.row),
513            Res::Medium => (self.col, self.row),
514            Res::High => (self.col, self.row / 2),
515        }
516    }
517
518    fn text_size(&self) -> (usize, usize) {
519        (self.width() / 8, self.height() / self.res.cell_h())
520    }
521
522    fn set_res(&mut self, res: Res) {
523        self.res = res;
524        let (w, h) = res.size();
525        self.pixels = vec![0; w * h];
526        self.memory = vec![0; w * h];
527        let (cols, rows) = self.text_size();
528        self.col = self.col.min(cols - 1);
529        self.row = self.row.min(rows - 1);
530        let mask = res.colors() - 1;
531        self.fg &= mask;
532        self.bg &= mask;
533        if res == Res::Medium {
534            self.fg = 3;
535        }
536    }
537
538    fn desktop_palette(&mut self) {
539        self.regs = DESKTOP;
540        match self.res {
541            Res::Medium => self.regs[..4].copy_from_slice(&DESKTOP_MEDIUM),
542            Res::High => self.regs[..2].copy_from_slice(&[0x777, 0x000]),
543            Res::Low => {}
544        }
545    }
546
547    fn random(&mut self, (lo, hi): (i32, i32)) -> i32 {
548        self.seed ^= self.seed << 13;
549        self.seed ^= self.seed >> 17;
550        self.seed ^= self.seed << 5;
551        lo + (self.seed % ((hi - lo).max(0) as u32 + 1)) as i32
552    }
553
554    /// Bytes from the BBS.
555    pub fn feed(&mut self, data: &[u8]) {
556        self.buf.extend_from_slice(data);
557        let mut buf = std::mem::take(&mut self.buf);
558        let mut pos = 0;
559        while pos < buf.len() && self.hold.is_none() {
560            match self.mode {
561                Mode::Text => {
562                    let b = buf[pos];
563                    pos += 1;
564                    self.text_byte(b);
565                }
566                Mode::Command => {
567                    let mut r = Reader { data: &buf, pos };
568                    match self.command(&mut r) {
569                        Ok(chain) => {
570                            pos = r.pos;
571                            self.mode = if chain { Mode::Command } else { Mode::Text };
572                            // X 9: run the stored commands next.
573                            if let Some(run) = self.run_stored.take() {
574                                buf.splice(pos..pos, run);
575                            }
576                        }
577                        Err(Incomplete) => {
578                            self.looping = None;
579                            break;
580                        }
581                    }
582                }
583            }
584        }
585        buf.drain(..pos);
586        // A command that never ends shouldn't swallow everything after it.
587        if buf.len() > 1 << 20 {
588            buf.clear();
589            self.mode = Mode::Text;
590        }
591        self.buf = buf;
592    }
593
594    /// Carry on after a pause (`t`, `q`, a color cycle's step), once its
595    /// time has come.
596    pub fn poll(&mut self, now: Instant) {
597        if self.hold.is_some_and(|t| now >= t) {
598            self.hold = None;
599            if self.cycle.is_some() {
600                self.cycle_step();
601                if self.hold.is_some() {
602                    return;
603                }
604            }
605            self.feed(&[]);
606        }
607    }
608
609    /// A key was pressed: as on the ST, it cuts a pause or a color cycle
610    /// short.
611    pub fn interrupt(&mut self) {
612        if self.hold.take().is_some() {
613            self.cycle = None;
614            self.feed(&[]);
615        }
616    }
617
618    /// One step of a color cycle: each pen from `from` to `to` takes the
619    /// next one's color (the last the first's), and the BBS waits.
620    fn cycle_step(&mut self) {
621        let Some(c) = self.cycle.as_mut() else { return };
622        let (lo, hi) = (c.from.min(c.to), c.from.max(c.to));
623        let regs: Vec<usize> = (lo..=hi).map(|pen| self.res.register(pen) as usize).collect();
624        let mut colors: Vec<StColor> = regs.iter().map(|&r| self.regs[r]).collect();
625        if c.from < c.to {
626            colors.rotate_right(1);
627        } else {
628            colors.rotate_left(1);
629        }
630        for (&r, &col) in regs.iter().zip(&colors) {
631            self.regs[r] = col;
632        }
633        c.steps -= 1;
634        if c.steps == 0 {
635            self.cycle = None;
636        } else {
637            self.hold = Some(Instant::now() + c.step);
638        }
639    }
640
641    // ---- VT-52 text ----
642
643    fn text_byte(&mut self, b: u8) {
644        if self.g {
645            self.g = false;
646            if b == b'#' && self.esc == Esc::None {
647                self.mode = Mode::Command;
648                return;
649            }
650            self.vt52(b'G');
651        }
652        if b == b'G' && self.esc == Esc::None {
653            self.g = true;
654            return;
655        }
656        self.vt52(b);
657    }
658
659    fn vt52(&mut self, b: u8) {
660        let (cols, rows) = self.text_size();
661        match self.esc {
662            Esc::Start => {
663                self.esc = Esc::None;
664                self.vt52_escape(b);
665                return;
666            }
667            Esc::Row => {
668                self.esc = Esc::Col(b.saturating_sub(32));
669                return;
670            }
671            Esc::Col(r) => {
672                self.esc = Esc::None;
673                self.row = (r as usize).min(rows - 1);
674                self.col = (b.saturating_sub(32) as usize).min(cols - 1);
675                return;
676            }
677            Esc::Fg => {
678                self.esc = Esc::None;
679                self.fg = b & (self.res.colors() - 1);
680                return;
681            }
682            Esc::Bg => {
683                self.esc = Esc::None;
684                self.bg = b & (self.res.colors() - 1);
685                return;
686            }
687            Esc::None => {}
688        }
689        match b {
690            0x07 => self.bells += 1,
691            0x08 => {
692                self.col = self.col.saturating_sub(1);
693                if self.destructive_bs {
694                    self.glyph_cell(b' ');
695                }
696            }
697            0x09 => self.col = ((self.col / 8 + 1) * 8).min(cols - 1),
698            0x0A..=0x0C => self.line_feed(),
699            0x0D => self.col = 0,
700            0x1B => self.esc = Esc::Start,
701            0x00..=0x1F => {}
702            _ => {
703                if self.col >= cols {
704                    if !self.wrap {
705                        self.col = cols - 1;
706                    } else {
707                        self.col = 0;
708                        self.line_feed();
709                    }
710                }
711                self.glyph_cell(b);
712                self.col += 1;
713                if self.col >= cols && !self.wrap {
714                    self.col = cols - 1;
715                }
716            }
717        }
718    }
719
720    fn vt52_escape(&mut self, b: u8) {
721        let (cols, rows) = self.text_size();
722        match b {
723            b'A' => self.row = self.row.saturating_sub(1),
724            b'B' => self.row = (self.row + 1).min(rows - 1),
725            b'C' => self.col = (self.col + 1).min(cols - 1),
726            b'D' => self.col = self.col.saturating_sub(1),
727            b'E' => {
728                self.clear_text_rows(0, rows);
729                (self.col, self.row) = (0, 0);
730            }
731            b'H' => (self.col, self.row) = (0, 0),
732            b'I' => {
733                if self.row == 0 {
734                    self.scroll_text(0, false);
735                } else {
736                    self.row -= 1;
737                }
738            }
739            b'J' => {
740                self.clear_line_from(self.col);
741                self.clear_text_rows(self.row + 1, rows);
742            }
743            b'K' => self.clear_line_from(self.col),
744            b'L' => {
745                self.scroll_text(self.row, false);
746                self.col = 0;
747            }
748            b'M' => {
749                self.scroll_text(self.row, true);
750                self.col = 0;
751            }
752            b'Y' => self.esc = Esc::Row,
753            b'b' => self.esc = Esc::Fg,
754            b'c' => self.esc = Esc::Bg,
755            b'd' => {
756                self.clear_text_rows(0, self.row);
757                self.clear_line_to(self.col);
758            }
759            b'e' => self.cursor_on = true,
760            b'f' => self.cursor_on = false,
761            b'j' => self.saved = (self.col, self.row),
762            b'k' => (self.col, self.row) = self.saved,
763            b'l' => self.clear_text_rows(self.row, self.row + 1),
764            b'o' => self.clear_line_to(self.col),
765            b'p' => self.reverse = true,
766            b'q' => self.reverse = false,
767            b'v' => self.wrap = true,
768            b'w' => self.wrap = false,
769            _ => {}
770        }
771    }
772
773    fn line_feed(&mut self) {
774        let rows = self.text_size().1;
775        if self.row + 1 >= rows {
776            self.scroll_text(0, true);
777        } else {
778            self.row += 1;
779        }
780    }
781
782    /// Scroll the text rows from `top` down: up a row (`up`), or down,
783    /// leaving a blank row.
784    fn scroll_text(&mut self, top: usize, up: bool) {
785        let (w, h) = self.res.size();
786        let ch = self.res.cell_h();
787        let (start, end) = (top * ch * w, h * w);
788        if start >= end {
789            return;
790        }
791        if up {
792            self.pixels.copy_within(start + ch * w..end, start);
793            self.pixels[end - ch * w..end].fill(self.bg);
794        } else {
795            self.pixels.copy_within(start..end - ch * w, start + ch * w);
796            self.pixels[start..start + ch * w].fill(self.bg);
797        }
798    }
799
800    fn clear_text_rows(&mut self, from: usize, to: usize) {
801        let w = self.width();
802        let ch = self.res.cell_h();
803        let (a, b) = ((from * ch * w).min(self.pixels.len()), (to * ch * w).min(self.pixels.len()));
804        if a < b {
805            self.pixels[a..b].fill(self.bg);
806        }
807    }
808
809    fn clear_cells(&mut self, row: usize, from: usize, to: usize) {
810        let w = self.width();
811        let ch = self.res.cell_h();
812        for y in row * ch..(row + 1) * ch {
813            let line = &mut self.pixels[y * w..(y + 1) * w];
814            line[(from * 8).min(w)..(to * 8).min(w)].fill(self.bg);
815        }
816    }
817
818    fn clear_line_from(&mut self, col: usize) {
819        let cols = self.text_size().0;
820        self.clear_cells(self.row, col, cols);
821    }
822
823    fn clear_line_to(&mut self, col: usize) {
824        self.clear_cells(self.row, 0, col + 1);
825    }
826
827    /// A character at the cursor, in the text colors.
828    fn glyph_cell(&mut self, ch: u8) {
829        let w = self.width();
830        let h = self.res.cell_h();
831        let (fg, bg) = if self.reverse { (self.bg, self.fg) } else { (self.fg, self.bg) };
832        let (x0, y0) = (self.col * 8, self.row * h);
833        for gy in 0..h {
834            let bits = glyph_row(ch, gy, h);
835            let row = &mut self.pixels[(y0 + gy) * w + x0..][..8];
836            for (gx, p) in row.iter_mut().enumerate() {
837                *p = if bits & (0x80 >> gx) != 0 { fg } else { bg };
838            }
839        }
840    }
841
842    // ---- Drawing ----
843
844    fn mask(&self) -> u8 {
845        self.res.colors() - 1
846    }
847
848    /// A pixel in the writing mode: `on` is the pattern's bit there.
849    fn plot(&mut self, x: i32, y: i32, on: bool, color: u8, mode: u8) {
850        let (w, h) = self.res.size();
851        if x < 0 || y < 0 || x as usize >= w || y as usize >= h {
852            return;
853        }
854        let p = &mut self.pixels[y as usize * w + x as usize];
855        match mode {
856            2 if on => *p = color,
857            3 if on => *p ^= self.res.colors() - 1,
858            4 if !on => *p = color,
859            1 => *p = if on { color } else { 0 },
860            _ => {}
861        }
862    }
863
864    fn reg(&self, pen: u8) -> u8 {
865        self.res.register(pen)
866    }
867
868    /// The fill pattern's bit at a pixel.
869    fn pattern_bit(&self, x: i32, y: i32) -> bool {
870        let f = self.attrs.fill;
871        let fi = f.style.max(1) as usize - 1;
872        let word = match f.interior {
873            0 => return false,
874            1 => return true,
875            2 if fi < 8 => DITHER[fi * 4 + (y & 3) as usize],
876            2 => OEMPAT[((fi - 8).min(15)) * 8 + (y & 7) as usize],
877            3 if fi < 6 => HATCH0[fi * 8 + (y & 7) as usize],
878            3 => HATCH1[((fi - 6).min(5)) * 16 + (y & 15) as usize],
879            _ => self.user_patterns[fi.min(7)][(y & 15) as usize],
880        };
881        word & (0x8000 >> (x & 15)) != 0
882    }
883
884    /// A horizontal span of the fill.
885    fn fill_span(&mut self, x0: i32, x1: i32, y: i32) {
886        if self.attrs.fill.interior == 0 {
887            return;
888        }
889        let color = self.reg(self.attrs.fill.pen);
890        let mode = self.attrs.mode;
891        for x in x0.max(0)..=x1.min(self.width() as i32 - 1) {
892            let on = self.pattern_bit(x, y);
893            self.plot(x, y, on, color, mode);
894        }
895    }
896
897    /// A filled polygon (even-odd), and its outline if the perimeter is on.
898    fn polygon(&mut self, pts: &[(i32, i32)], perimeter: bool) {
899        if pts.is_empty() {
900            return;
901        }
902        let (ymin, ymax) = pts.iter().fold((i32::MAX, i32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
903        let h = self.height() as i32;
904        for y in ymin.max(0)..=ymax.min(h - 1) {
905            let mut xs: Vec<i32> = Vec::new();
906            for i in 0..pts.len() {
907                let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
908                let (lo, hi) = if a.1 < b.1 { (a, b) } else { (b, a) };
909                if y >= lo.1 && y < hi.1 || y == ymax && hi.1 == ymax && lo.1 != hi.1 {
910                    let t = (y - lo.1) as f64 / (hi.1 - lo.1) as f64;
911                    xs.push((lo.0 as f64 + t * (hi.0 - lo.0) as f64).round() as i32);
912                }
913            }
914            xs.sort_unstable();
915            for pair in xs.chunks(2) {
916                if let [a, b] = pair {
917                    self.fill_span(*a, *b, y);
918                }
919            }
920        }
921        if perimeter {
922            let color = self.reg(self.attrs.fill.pen);
923            let mut closed = pts.to_vec();
924            closed.push(pts[0]);
925            for seg in closed.windows(2) {
926                self.thin_line(seg[0], seg[1], color, 0xFFFF, self.attrs.mode);
927            }
928        }
929    }
930
931    /// A one-pixel line in a style.
932    fn thin_line(&mut self, (x0, y0): (i32, i32), (x1, y1): (i32, i32), color: u8, style: u16, mode: u8) {
933        let (dx, dy) = ((x1 - x0).abs(), -(y1 - y0).abs());
934        let (sx, sy) = (if x0 < x1 { 1 } else { -1 }, if y0 < y1 { 1 } else { -1 });
935        let (mut x, mut y, mut err) = (x0, y0, dx + dy);
936        let mut bit = 0u32;
937        loop {
938            let on = style.rotate_left(bit) & 0x8000 != 0;
939            // Lines draw their pattern's zero bits only when replacing.
940            if on || mode == 1 || mode == 4 {
941                self.plot(x, y, on, color, mode);
942            }
943            bit += 1;
944            if x == x1 && y == y1 {
945                break;
946            }
947            let e2 = 2 * err;
948            if e2 >= dy {
949                err += dy;
950                x += sx;
951            }
952            if e2 <= dx {
953                err += dx;
954                y += sy;
955            }
956        }
957    }
958
959    /// A line in the line attributes: wide lines are filled polygons,
960    /// with square, round or arrow ends.
961    fn line(&mut self, a: (i32, i32), b: (i32, i32)) {
962        let l = self.attrs.line;
963        let color = self.reg(l.pen);
964        let style = match l.style {
965            1..=6 => LINE_STYLES[l.style as usize - 1],
966            _ => {
967                let p = self.user_patterns[6];
968                p[0]
969            }
970        };
971        if l.width <= 1 {
972            self.thin_line(a, b, color, style, self.attrs.mode);
973            self.line_ends(a, b, color, 1);
974            return;
975        }
976        let saved = self.attrs.fill;
977        self.attrs.fill = FillAttr { pen: l.pen, interior: 1, style: 1, perimeter: false };
978        let (dx, dy) = ((b.0 - a.0) as f64, (b.1 - a.1) as f64);
979        let len = (dx * dx + dy * dy).sqrt().max(1.0);
980        let half = l.width as f64 / 2.0;
981        // Perpendicular, with y squashed for the screen's pixel shape.
982        let (px, py) = (-dy / len * half, dx / len * half * self.res.aspect());
983        let q = |p: (i32, i32), s: f64| ((p.0 as f64 + px * s).round() as i32, (p.1 as f64 + py * s).round() as i32);
984        self.polygon(&[q(a, 1.0), q(b, 1.0), q(b, -1.0), q(a, -1.0)], false);
985        self.line_ends(a, b, color, l.width);
986        self.attrs.fill = saved;
987    }
988
989    fn line_ends(&mut self, a: (i32, i32), b: (i32, i32), color: u8, width: i32) {
990        let ends = self.attrs.line.ends;
991        for (end, (at, from)) in [(ends.0, (a, b)), (ends.1, (b, a))] {
992            match end {
993                1 => self.arrow(at, from, color, width),
994                2 if width > 1 => {
995                    let r = width / 2;
996                    let saved = self.attrs.fill;
997                    self.attrs.fill = FillAttr { pen: self.attrs.line.pen, interior: 1, style: 1, perimeter: false };
998                    let pts = self.ellipse_points(at, r, (r as f64 * self.res.aspect()) as i32, 0, 3600);
999                    self.polygon(&pts, false);
1000                    self.attrs.fill = saved;
1001                }
1002                _ => {}
1003            }
1004        }
1005    }
1006
1007    fn arrow(&mut self, tip: (i32, i32), from: (i32, i32), _color: u8, width: i32) {
1008        let (dx, dy) = ((tip.0 - from.0) as f64, (tip.1 - from.1) as f64);
1009        let len = (dx * dx + dy * dy).sqrt().max(1.0);
1010        let size = (8 + 2 * width) as f64;
1011        let (ux, uy) = (dx / len, dy / len);
1012        let back = (tip.0 as f64 - ux * size, tip.1 as f64 - uy * size);
1013        let side = size / 2.0;
1014        let p1 = ((back.0 - uy * side) as i32, (back.1 + ux * side * self.res.aspect()) as i32);
1015        let p2 = ((back.0 + uy * side) as i32, (back.1 - ux * side * self.res.aspect()) as i32);
1016        let saved = self.attrs.fill;
1017        self.attrs.fill = FillAttr { pen: self.attrs.line.pen, interior: 1, style: 1, perimeter: false };
1018        self.polygon(&[tip, p1, p2], false);
1019        self.attrs.fill = saved;
1020    }
1021
1022    /// Points around an ellipse from `start` to `end`, tenths of a degree
1023    /// counter-clockwise from three o'clock (the VDI's way).
1024    fn ellipse_points(&self, c: (i32, i32), rx: i32, ry: i32, start: i32, end: i32) -> Vec<(i32, i32)> {
1025        let mut span = end - start;
1026        if span <= 0 {
1027            span += 3600;
1028        }
1029        let steps = (rx.max(ry) / 4).clamp(32, 128);
1030        (0..=steps)
1031            .map(|i| {
1032                let a = (start as f64 + span as f64 * i as f64 / steps as f64).to_radians() / 10.0;
1033                ((c.0 as f64 + rx as f64 * a.cos()).round() as i32, (c.1 as f64 - ry as f64 * a.sin()).round() as i32)
1034            })
1035            .collect()
1036    }
1037
1038    fn polyline(&mut self, pts: &[(i32, i32)]) {
1039        for seg in pts.windows(2) {
1040            self.line(seg[0], seg[1]);
1041        }
1042    }
1043
1044    /// A filled shape: interior, then its outline if the perimeter's on.
1045    fn shape(&mut self, pts: &[(i32, i32)]) {
1046        self.polygon(pts, self.attrs.fill.perimeter);
1047    }
1048
1049    fn rounded_box(&mut self, (x0, y0, x1, y1): (i32, i32, i32, i32), filled: bool) {
1050        let (x0, x1) = (x0.min(x1), x0.max(x1));
1051        let (y0, y1) = (y0.min(y1), y0.max(y1));
1052        let rx = ((x1 - x0) / 2).min(8);
1053        let ry = (((y1 - y0) / 2) as f64).min(8.0 * self.res.aspect()) as i32;
1054        let mut pts = Vec::new();
1055        for (cx, cy, a0) in
1056            [(x1 - rx, y0 + ry, 0), (x0 + rx, y0 + ry, 900), (x0 + rx, y1 - ry, 1800), (x1 - rx, y1 - ry, 2700)]
1057        {
1058            pts.extend(self.ellipse_points((cx, cy), rx, ry, a0, a0 + 900));
1059        }
1060        if filled {
1061            self.shape(&pts);
1062        } else {
1063            pts.push(pts[0]);
1064            self.polyline(&pts);
1065        }
1066    }
1067
1068    /// Fill the area of one color around a point (`F`).
1069    fn flood(&mut self, x: i32, y: i32) {
1070        let (w, h) = self.res.size();
1071        if x < 0 || y < 0 || x as usize >= w || y as usize >= h {
1072            return;
1073        }
1074        let target = self.pixels[y as usize * w + x as usize];
1075        let mut seen = vec![false; w * h];
1076        let mut stack = vec![(x as usize, y as usize)];
1077        let mut spans = Vec::new();
1078        while let Some((x, y)) = stack.pop() {
1079            if seen[y * w + x] || self.pixels[y * w + x] != target {
1080                continue;
1081            }
1082            let (mut a, mut b) = (x, x);
1083            while a > 0 && !seen[y * w + a - 1] && self.pixels[y * w + a - 1] == target {
1084                a -= 1;
1085            }
1086            while b + 1 < w && !seen[y * w + b + 1] && self.pixels[y * w + b + 1] == target {
1087                b += 1;
1088            }
1089            for i in a..=b {
1090                seen[y * w + i] = true;
1091                for ny in [y.wrapping_sub(1), y + 1] {
1092                    if ny < h && !seen[ny * w + i] && self.pixels[ny * w + i] == target {
1093                        stack.push((i, ny));
1094                    }
1095                }
1096            }
1097            spans.push((a as i32, b as i32, y as i32));
1098        }
1099        for (a, b, y) in spans {
1100            self.fill_span(a, b, y);
1101        }
1102    }
1103
1104    fn marker(&mut self, x: i32, y: i32) {
1105        let (pen, kind, size) = self.attrs.marker;
1106        let color = self.reg(pen);
1107        let shape = MARKERS[(kind.clamp(1, 6) - 1) as usize];
1108        let scale = if kind == 1 { 0.0 } else { size.max(1) as f64 };
1109        for poly in shape {
1110            let pts: Vec<(i32, i32)> =
1111                poly.iter().map(|&(dx, dy)| (x + (dx as f64 * scale) as i32, y + (dy as f64 * scale) as i32)).collect();
1112            for seg in pts.windows(2) {
1113                self.thin_line(seg[0], seg[1], color, 0xFFFF, self.attrs.mode);
1114            }
1115        }
1116    }
1117
1118    /// Text at a point (its baseline), as `W` writes it.
1119    fn write_text(&mut self, x: i32, y: i32, text: &[u8]) {
1120        let t = self.attrs.text;
1121        let color = self.reg(t.pen);
1122        // The system font's sizes, as the ST has them: 6x6 at 8 points,
1123        // 8x8 at 9, 8x16 at 10, and twice those at 16, 18 and 20.
1124        let (cell, scale) = match t.points {
1125            ..=8 => (6, 1),
1126            9 => (8, 1),
1127            10..=15 => (16, 1),
1128            16..=17 => (6, 2),
1129            18..=19 => (8, 2),
1130            _ => (16, 2),
1131        };
1132        let ascent = match cell {
1133            6 => 4,
1134            8 => 6,
1135            _ => 13,
1136        } * scale;
1137        let (cw, ch) = (cell.min(8) * scale, cell * scale);
1138        for (i, &c) in text.iter().enumerate() {
1139            for gy in 0..ch {
1140                let mut bits = glyph_row(c, (gy / scale) as usize, cell as usize) as u32;
1141                if t.effects & 1 != 0 {
1142                    bits |= bits >> 1;
1143                }
1144                if t.effects & 2 != 0 && gy % 2 == 1 {
1145                    bits &= 0x55;
1146                }
1147                if t.effects & 8 != 0 && gy == ascent + scale {
1148                    bits = 0xFF;
1149                }
1150                let skew = if t.effects & 4 != 0 { (ch - gy) / 4 } else { 0 };
1151                for gx in 0..cw {
1152                    let on = bits & (0x80 >> (gx / scale)) != 0;
1153                    if !on && self.attrs.mode != 1 {
1154                        continue;
1155                    }
1156                    // Along the text and down from its top, turned.
1157                    let (ax, ay) = ((i as i32) * cw + gx + skew, gy - ascent);
1158                    let (px, py) = match t.rotation % 4 {
1159                        1 => (x + ay, y - ax),
1160                        2 => (x - ax, y - ay),
1161                        3 => (x - ay, y + ax),
1162                        _ => (x + ax, y + ay),
1163                    };
1164                    self.plot(px, py, on, color, self.attrs.mode);
1165                }
1166            }
1167        }
1168    }
1169
1170    /// `G`: copy pixels between the screen and the blit buffer, combining
1171    /// them with one of the 16 logic operations.
1172    fn blit(&mut self, from_memory: bool, src: (i32, i32, i32, i32), to_memory: bool, dst: (i32, i32), op: u8) {
1173        let (w, h) = self.res.size();
1174        let (x0, y0, x1, y1) = (src.0.min(src.2), src.1.min(src.3), src.0.max(src.2), src.1.max(src.3));
1175        let source: Vec<u8> = if from_memory { self.memory.clone() } else { self.pixels.clone() };
1176        let mask = self.mask();
1177        for y in 0..=(y1 - y0) {
1178            for x in 0..=(x1 - x0) {
1179                let (sx, sy, dx, dy) = (x0 + x, y0 + y, dst.0 + x, dst.1 + y);
1180                if sx < 0 || sy < 0 || dx < 0 || dy < 0 {
1181                    continue;
1182                }
1183                let (sx, sy, dx, dy) = (sx as usize, sy as usize, dx as usize, dy as usize);
1184                if sx >= w || sy >= h || dx >= w || dy >= h {
1185                    continue;
1186                }
1187                let s = source[sy * w + sx];
1188                let target = if to_memory { &mut self.memory } else { &mut self.pixels };
1189                let d = target[dy * w + dx];
1190                let v = match op {
1191                    0 => 0,
1192                    1 => s & d,
1193                    2 => s & !d,
1194                    4 => !s & d,
1195                    5 => d,
1196                    6 => s ^ d,
1197                    7 => s | d,
1198                    8 => !(s | d),
1199                    9 => !(s ^ d),
1200                    10 => !d,
1201                    11 => s | !d,
1202                    12 => !s,
1203                    13 => !s | d,
1204                    14 => !(s & d),
1205                    15 => 0xFF,
1206                    _ => s,
1207                };
1208                target[dy * w + dx] = v & mask;
1209            }
1210        }
1211    }
1212
1213    fn clear_screen(&mut self) {
1214        self.pixels.fill(0);
1215    }
1216}
1217
1218/// A row of a glyph, 6, 8 or 16 high (CP437 at 8 and 16; ASCII at 6,
1219/// where anything else comes from the 8x8 font).
1220fn glyph_row(c: u8, y: usize, h: usize) -> u8 {
1221    match h {
1222        16 => FONT16[c as usize * 16 + y.min(15)],
1223        6 if (0x20..0x7F).contains(&c) => FONT6[c as usize * 6 + y.min(5)],
1224        6 => FONT8[c as usize * 8 + (y * 8 / 6).min(7)],
1225        _ => FONT8[c as usize * 8 + y.min(7)],
1226    }
1227}
1228
1229// The command interpreter, in its own impl for its size.
1230impl Igs {
1231    /// A number, from the stream or a running loop, scaled by `g` if it's
1232    /// a coordinate (`axis` 1 for x, 2 for y).
1233    fn val(&mut self, r: &mut Reader, axis: u8) -> Res2<i32> {
1234        let tok = match self.looping.as_mut() {
1235            Some(lp) if lp.running || lp.at != 0 => lp.next(),
1236            _ => r.token()?,
1237        };
1238        let mut n = match tok {
1239            Tok::Num(n) | Tok::Plus(n) | Tok::Bang(n) => n,
1240            Tok::Minus(n) => -n,
1241            Tok::Rand => self.random(self.rand),
1242            Tok::RandBig => self.random(self.rand_big),
1243            Tok::X | Tok::Y => 0,
1244        };
1245        if self.scaling == 1 && axis > 0 && n > 0 {
1246            let max = if axis == 1 { self.width() - 1 } else { self.height() - 1 } as f64;
1247            n = (n as f64 * max / 9999.0).round() as i32;
1248        } else if self.scaling == 2 && axis == 2 && self.res == Res::High {
1249            n *= 2;
1250        }
1251        Ok(n)
1252    }
1253
1254    fn point(&mut self, r: &mut Reader) -> Res2<(i32, i32)> {
1255        Ok((self.val(r, 1)?, self.val(r, 2)?))
1256    }
1257
1258    /// One command, its letter next in `r`. Returns whether a chained
1259    /// command follows.
1260    fn command(&mut self, r: &mut Reader) -> Res2<bool> {
1261        let mut key = r.next()?;
1262        // A new G# where a chained command was expected starts over.
1263        while key == b'G' && r.data.get(r.pos) == Some(&b'#') {
1264            r.pos += 1;
1265            key = r.next()?;
1266        }
1267        let chain = r.next()? == b'>';
1268        if key == b'&' {
1269            self.loop_command(r)?;
1270        } else {
1271            self.run(key, r)?;
1272        }
1273        Ok(chain)
1274    }
1275
1276    /// `&`: run a command (or a "chain gang" of them) many times.
1277    fn loop_command(&mut self, r: &mut Reader) -> Res2<()> {
1278        let (from, to, step, _delay) = (r.token()?, r.token()?, r.token()?, r.token()?);
1279        let num = |t: Tok| if let Tok::Num(n) = t { n } else { 0 };
1280        let (from, to, step) = (num(from), num(to), num(step));
1281        let mut step = if from > to { -step } else { step };
1282        if step == 0 {
1283            step = 1;
1284        }
1285        let key = r.next()?;
1286        let gang = if key == b'>' { Some(r.text()?) } else { None };
1287        let kind = r.next()?;
1288        let count = (num(r.token()?)).clamp(1, 2048) as usize;
1289        let mut params = Vec::with_capacity(count);
1290        for _ in 0..count {
1291            params.push(r.token()?);
1292        }
1293        let plain = !params.iter().any(|t| !matches!(t, Tok::Num(_) | Tok::Rand | Tok::RandBig));
1294        self.looping = Some(Loop {
1295            params,
1296            at: 0,
1297            value: from,
1298            to,
1299            step,
1300            from,
1301            kind,
1302            x_count: 0,
1303            first: 0,
1304            second: 0,
1305            was: from,
1306            plain,
1307            running: true,
1308        });
1309        let mut guard = 0;
1310        loop {
1311            let cmd = match &gang {
1312                Some(g) => {
1313                    let i = self.val(r, 0)?.clamp(0, 127) as usize;
1314                    g.get(i).copied().unwrap_or(b' ')
1315                }
1316                None => key,
1317            };
1318            // W reads its text from the stream each time only with `@`.
1319            self.run(cmd, r)?;
1320            guard += 1;
1321            let done = self.looping.as_ref().is_none_or(|lp| !lp.running && lp.at == 0);
1322            if done || guard > 100_000 {
1323                break;
1324            }
1325        }
1326        self.looping = None;
1327        Ok(())
1328    }
1329
1330    fn in_loop(&self) -> bool {
1331        self.looping.is_some()
1332    }
1333
1334    fn run(&mut self, key: u8, r: &mut Reader) -> Res2<()> {
1335        match key {
1336            b'A' => {
1337                let (t, p, b) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1338                let f = &mut self.attrs.fill;
1339                f.interior = if (0..=4).contains(&t) { t as u8 } else { 0 };
1340                if f.interior == 4 && p >= 8 {
1341                    // 8: random sand in slot 0; 9 and up: its default.
1342                    self.user_patterns[0] = if p == 8 {
1343                        std::array::from_fn(|_| {
1344                            self.seed ^= self.seed << 13;
1345                            self.seed ^= self.seed >> 17;
1346                            self.seed ^= self.seed << 5;
1347                            (self.seed & self.seed >> 7) as u16
1348                        })
1349                    } else {
1350                        ROM_UD_PATTERN
1351                    };
1352                    self.attrs.fill.style = 1;
1353                } else {
1354                    self.attrs.fill.style =
1355                        if self.attrs.fill.interior == 4 { (p + 1).clamp(1, 8) as u8 } else { p.clamp(1, 24) as u8 };
1356                }
1357                self.attrs.fill.perimeter = b != 0;
1358            }
1359            b'B' => {
1360                let (a, b, round) = (self.point(r)?, self.point(r)?, self.val(r, 0)?);
1361                if round != 0 {
1362                    self.rounded_box((a.0, a.1, b.0, b.1), true);
1363                } else {
1364                    self.shape(&[(a.0, a.1), (b.0, a.1), (b.0, b.1), (a.0, b.1)]);
1365                }
1366            }
1367            b'C' => {
1368                let (which, pen) = (self.val(r, 0)?, self.val(r, 0)?);
1369                let pen = if (0..=15).contains(&pen) { pen as u8 } else { 0 };
1370                match which {
1371                    0 => self.attrs.marker.0 = pen,
1372                    1 => self.attrs.line.pen = pen,
1373                    2 => self.attrs.fill.pen = pen,
1374                    3 => self.attrs.text.pen = pen,
1375                    _ => self.attrs.marker.0 = pen,
1376                }
1377            }
1378            b'D' => {
1379                let to = self.point(r)?;
1380                let from = self.pen_at;
1381                self.line(from, to);
1382                self.pen_at = to;
1383            }
1384            b'E' => {
1385                let (fx, size, rot) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1386                self.attrs.text.effects = fx as u8;
1387                self.attrs.text.points = size.clamp(8, 20);
1388                self.attrs.text.rotation = if (0..=4).contains(&rot) { rot as u8 } else { 0 };
1389            }
1390            b'F' => {
1391                let (x, y) = self.point(r)?;
1392                self.flood(x, y);
1393            }
1394            b'f' | b'z' => {
1395                let n = self.val(r, 0)?.clamp(0, 128) as usize;
1396                let mut pts = Vec::with_capacity(n);
1397                for _ in 0..n {
1398                    pts.push(self.point(r)?);
1399                }
1400                if key == b'f' {
1401                    self.shape(&pts);
1402                } else if n >= 2 {
1403                    self.polyline(&pts);
1404                }
1405            }
1406            b'G' => self.grab(r)?,
1407            b'g' => {
1408                let v = self.val(r, 0)?;
1409                self.scaling = if (0..=2).contains(&v) { v } else { 0 };
1410            }
1411            b'H' => {
1412                self.hollow = self.val(r, 0)? != 0;
1413                if self.hollow {
1414                    self.attrs.fill.interior = 0;
1415                    self.attrs.mode = 2;
1416                    self.attrs.fill.perimeter = true;
1417                } else {
1418                    self.attrs.fill.interior = 1;
1419                    self.attrs.mode = 1;
1420                    self.attrs.fill.perimeter = false;
1421                }
1422            }
1423            b'I' => match self.val(r, 0)? {
1424                1 => self.desktop_palette(),
1425                2 => self.attrs = Attrs::default(),
1426                3 => self.regs = IG_PALETTE,
1427                4 => self.desktop_palette(),
1428                5 => {
1429                    if self.res == Res::Low {
1430                        self.set_res(Res::Medium);
1431                    }
1432                }
1433                _ => {
1434                    self.desktop_palette();
1435                    self.attrs = Attrs::default();
1436                }
1437            },
1438            b'J' | b'Y' => {
1439                let (c, rx, ry) = (self.point(r)?, self.val(r, 1)?, self.val(r, 2)?);
1440                let (a0, a1) = (self.val(r, 0)? * 10, self.val(r, 0)? * 10);
1441                let pts = self.ellipse_points(c, rx, ry, a0, a1);
1442                if key == b'J' {
1443                    self.polyline(&pts);
1444                } else {
1445                    let mut pie = vec![c];
1446                    pie.extend(pts);
1447                    self.shape(&pie);
1448                }
1449            }
1450            b'K' | b'V' => {
1451                let (c, rad) = (self.point(r)?, self.val(r, 1)?);
1452                let (a0, a1) = (self.val(r, 0)? * 10, self.val(r, 0)? * 10);
1453                let ry = (rad as f64 * self.res.aspect()).round() as i32;
1454                let pts = self.ellipse_points(c, rad, ry, a0, a1);
1455                if key == b'K' {
1456                    self.polyline(&pts);
1457                } else {
1458                    let mut pie = vec![c];
1459                    pie.extend(pts);
1460                    self.shape(&pie);
1461                }
1462            }
1463            b'k' => match self.val(r, 0)? {
1464                0 => self.cursor_on = false,
1465                1 => self.cursor_on = true,
1466                2 => self.destructive_bs = true,
1467                _ => self.destructive_bs = false,
1468            },
1469            b'L' => {
1470                let (a, b) = (self.point(r)?, self.point(r)?);
1471                self.line(a, b);
1472                self.pen_at = b;
1473            }
1474            b'M' => {
1475                let m = self.val(r, 0)?;
1476                self.attrs.mode = if (1..=4).contains(&m) { m as u8 } else { 1 };
1477            }
1478            b'N' => self.noise(r)?,
1479            b'n' => {
1480                for _ in 0..6 {
1481                    self.val(r, 0)?;
1482                }
1483            }
1484            b'O' => {
1485                let (c, rad) = (self.point(r)?, self.val(r, 1)?);
1486                let ry = (rad as f64 * self.res.aspect()).round() as i32;
1487                let pts = self.ellipse_points(c, rad, ry, 0, 3600);
1488                self.shape(&pts);
1489            }
1490            b'P' => {
1491                let p = self.point(r)?;
1492                self.marker(p.0, p.1);
1493                self.pen_at = p;
1494            }
1495            b'Q' => {
1496                let (c, rx, ry) = (self.point(r)?, self.val(r, 1)?, self.val(r, 2)?);
1497                let pts = self.ellipse_points(c, rx, ry, 0, 3600);
1498                self.shape(&pts);
1499            }
1500            b'q' => {
1501                let n = self.val(r, 0)?;
1502                if (1..=180).contains(&n) && !self.in_loop() {
1503                    self.hold = Some(Instant::now() + Duration::from_millis(n as u64 * 1000 / 60));
1504                }
1505            }
1506            b'R' => {
1507                let (res, pal) = (self.val(r, 0)?, self.val(r, 0)?);
1508                let want = match res {
1509                    0 => Res::Low,
1510                    1 => Res::Medium,
1511                    _ => self.res,
1512                };
1513                if self.res != Res::High && want != self.res {
1514                    self.set_res(want);
1515                }
1516                match pal {
1517                    1 | 3 => self.desktop_palette(),
1518                    2 => self.regs = IG_PALETTE,
1519                    _ => {}
1520                }
1521            }
1522            b'S' => {
1523                let pen = self.val(r, 0)?;
1524                let (cr, cg, cb) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1525                let pen = if (0..=15).contains(&pen) { pen as u8 } else { 0 };
1526                let c = |v: i32| v.clamp(0, 7) as u16;
1527                let reg = self.reg(pen) as usize;
1528                self.regs[reg] = c(cr) << 8 | c(cg) << 4 | c(cb);
1529            }
1530            b's' => match self.val(r, 0)? {
1531                0 => self.vt52_escape(b'E'),
1532                1 => self.vt52_escape(b'd'),
1533                2 => self.vt52_escape(b'J'),
1534                3 => self.clear_screen(),
1535                4 => {
1536                    self.clear_screen();
1537                    (self.col, self.row) = (0, 0);
1538                }
1539                _ => {
1540                    self.reverse = false;
1541                    self.bg = 0;
1542                    self.fg = 3 & self.mask();
1543                    self.vt52_escape(b'E');
1544                }
1545            },
1546            b'T' => {
1547                let which = self.val(r, 0)?;
1548                if !(1..=2).contains(&which) {
1549                    return Ok(());
1550                }
1551                let kind = self.val(r, 0)?;
1552                let size = self.val(r, 0)?;
1553                if which == 1 {
1554                    self.attrs.marker.1 = if (1..=6).contains(&kind) { kind as u8 } else { 1 };
1555                    self.attrs.marker.2 = if (1..=8).contains(&size) { size } else { 1 };
1556                } else {
1557                    let kind = if (1..=7).contains(&kind) { kind as u8 } else { 1 };
1558                    if (1..42).contains(&size) {
1559                        self.attrs.line.style = kind;
1560                        self.attrs.line.width = if kind > 1 { 1 } else { size };
1561                    }
1562                    let ends = match size {
1563                        0 => Some((0, 0)),
1564                        50 => Some((1, 1)),
1565                        51 => Some((1, 0)),
1566                        52 => Some((0, 1)),
1567                        53 => Some((1, 2)),
1568                        54 => Some((2, 1)),
1569                        60 => Some((2, 2)),
1570                        61 => Some((2, 0)),
1571                        62 => Some((0, 2)),
1572                        63 => Some((2, 1)),
1573                        64 => Some((1, 2)),
1574                        _ => None,
1575                    };
1576                    if let Some(e) = ends {
1577                        self.attrs.line.ends = e;
1578                    }
1579                }
1580            }
1581            b't' => {
1582                let n = self.val(r, 0)?;
1583                if n > 0 && !self.in_loop() {
1584                    self.hold = Some(Instant::now() + Duration::from_secs(n.min(30) as u64));
1585                }
1586            }
1587            b'U' => {
1588                let (a, b, filled) = (self.point(r)?, self.point(r)?, self.val(r, 0)?);
1589                self.rounded_box((a.0, a.1, b.0, b.1), filled != 0);
1590            }
1591            b'W' => {
1592                let p = self.point(r)?;
1593                let reads = self.looping.as_ref().is_none_or(|lp| lp.kind == b'@');
1594                if reads {
1595                    self.last_text = r.text()?;
1596                }
1597                let text = self.last_text.clone();
1598                self.write_text(p.0, p.1, &text);
1599            }
1600            b'X' => self.extended(r)?,
1601            b'Z' => {
1602                let (a, b) = (self.point(r)?, self.point(r)?);
1603                let (x0, x1, y0, y1) = (a.0.min(b.0), a.0.max(b.0), a.1.min(b.1), a.1.max(b.1));
1604                for y in y0..=y1 {
1605                    self.fill_span(x0, x1, y);
1606                }
1607            }
1608            b'b' => match self.val(r, 0)? {
1609                20 => {
1610                    for _ in 0..6 {
1611                        self.val(r, 0)?;
1612                    }
1613                }
1614                22 | 23 => {
1615                    self.val(r, 0)?;
1616                }
1617                _ => {}
1618            },
1619            b'<' => {
1620                let (cr, kind, show) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1621                if (2..=10).contains(&kind) {
1622                    self.awaiting_zone = Some((cr != 0, show < 2));
1623                }
1624            }
1625            b'?' => {
1626                let q = self.val(r, 0)?;
1627                match q {
1628                    0 => self.replies.extend_from_slice(VERSION),
1629                    1 => {
1630                        self.val(r, 0)?;
1631                        let (c, rw) = (self.col as u8 + 32, self.row as u8 + 32);
1632                        self.replies.extend_from_slice(&[c, rw, 32]);
1633                    }
1634                    2 => {
1635                        self.val(r, 0)?;
1636                        self.replies.extend_from_slice(b"0,0,0:");
1637                    }
1638                    3 => self.replies.extend_from_slice(&[b'0' + self.res.number(), b':']),
1639                    _ => {}
1640                }
1641            }
1642            // The modified VT-52 commands.
1643            b'c' => {
1644                let (which, reg) = (self.val(r, 0)?, self.val(r, 0)?);
1645                let reg = reg as u8 & self.mask();
1646                if which == 0 {
1647                    self.bg = reg;
1648                } else {
1649                    self.fg = reg;
1650                }
1651            }
1652            b'd' => {
1653                for _ in 0..self.val(r, 0)?.clamp(0, 50) {
1654                    self.vt52_escape(b'M');
1655                }
1656            }
1657            b'i' => {
1658                let (kind, n) = (self.val(r, 0)?, self.val(r, 0)?);
1659                for _ in 0..n.clamp(0, 50) {
1660                    if kind == 0 {
1661                        self.vt52_escape(b'I');
1662                    } else {
1663                        self.vt52_escape(b'L');
1664                    }
1665                }
1666            }
1667            b'l' => match self.val(r, 0)? {
1668                0 => {
1669                    self.vt52_escape(b'l');
1670                    self.col = 0;
1671                }
1672                1 => self.vt52_escape(b'o'),
1673                _ => self.vt52_escape(b'K'),
1674            },
1675            b'm' => {
1676                let (dir, n) = (self.val(r, 0)?, self.val(r, 0)?);
1677                let esc = match dir {
1678                    1 => b'A',
1679                    2 => b'B',
1680                    3 => b'C',
1681                    4 => b'D',
1682                    _ => b'H',
1683                };
1684                for _ in 0..n.clamp(1, 80) {
1685                    self.vt52_escape(esc);
1686                }
1687            }
1688            b'p' => {
1689                let (c, rw) = (self.val(r, 0)?, self.val(r, 0)?);
1690                let (cols, rows) = self.text_size();
1691                self.col = (c.max(0) as usize).min(cols - 1);
1692                self.row = (rw.max(0) as usize).min(rows - 1);
1693            }
1694            b'r' => {
1695                if self.val(r, 0)? == 0 {
1696                    self.saved = (self.col, self.row);
1697                } else {
1698                    (self.col, self.row) = self.saved;
1699                }
1700            }
1701            b'v' => self.reverse = self.val(r, 0)? != 0,
1702            b'w' => self.wrap = self.val(r, 0)? != 0,
1703            _ => log::debug!("unknown IGS command {:?}", key as char),
1704        }
1705        Ok(())
1706    }
1707
1708    fn grab(&mut self, r: &mut Reader) -> Res2<()> {
1709        let (kind, op) = (self.val(r, 0)?, self.val(r, 0)?);
1710        let op = if (0..=15).contains(&op) { op as u8 } else { 3 };
1711        let a = self.point(r)?;
1712        match kind {
1713            0 => {
1714                let b = self.point(r)?;
1715                let d = self.point(r)?;
1716                self.blit(false, (a.0, a.1, b.0, b.1), false, d, op);
1717            }
1718            1 => {
1719                let b = self.point(r)?;
1720                self.memory_rect = (a.0, a.1, b.0, b.1);
1721                self.blit(false, self.memory_rect, true, (a.0.min(b.0), a.1.min(b.1)), op);
1722            }
1723            2 => self.blit(true, self.memory_rect, false, a, op),
1724            _ => {
1725                let b = self.point(r)?;
1726                let d = self.point(r)?;
1727                self.blit(true, (a.0, a.1, b.0, b.1), kind == 4, d, op);
1728            }
1729        }
1730        Ok(())
1731    }
1732
1733    /// `N`: MIDI and sound chip data, read past.
1734    fn noise(&mut self, r: &mut Reader) -> Res2<()> {
1735        match self.val(r, 0)? {
1736            0 | 1 | 3 | 4 => {
1737                let n = self.val(r, 0)?.clamp(0, 10_001) as usize;
1738                for _ in 0..n {
1739                    r.next()?;
1740                }
1741            }
1742            6 => {
1743                self.val(r, 0)?;
1744                self.val(r, 0)?;
1745            }
1746            _ => {}
1747        }
1748        Ok(())
1749    }
1750
1751    fn extended(&mut self, r: &mut Reader) -> Res2<()> {
1752        match self.val(r, 0)? {
1753            // Spray paint: markers at random in a rectangle.
1754            0 => {
1755                let (a, rx, ry, n) = (self.point(r)?, self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1756                for _ in 0..n.clamp(0, 9999) {
1757                    let x = a.0 + self.random((0, rx.clamp(0, 255)));
1758                    let y = a.1 + self.random((0, ry.clamp(0, 255)));
1759                    self.marker(x, y);
1760                }
1761            }
1762            // A color register, as the ST stores it (0x0RGB, in decimal).
1763            1 => {
1764                let (reg, v) = (self.val(r, 0)?, self.val(r, 0)?);
1765                self.regs[(reg & 15) as usize] = (v as u16) & 0x777;
1766            }
1767            2 => {
1768                let (a, b) = (self.val(r, 0)?, self.val(r, 0)?);
1769                if a == b {
1770                    let c = self.val(r, 0)?;
1771                    self.rand_big = (b, c);
1772                } else {
1773                    self.rand = (a, b);
1774                }
1775            }
1776            // The right mouse button's text: read past.
1777            3 => match self.val(r, 0)? {
1778                1 => {
1779                    self.val(r, 0)?;
1780                }
1781                2 => {
1782                    let (_on, _cr, len) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1783                    for _ in 0..len.clamp(0, 80) + 1 {
1784                        r.next()?;
1785                    }
1786                }
1787                _ => {}
1788            },
1789            // A mouse zone, and what it sends.
1790            4 => {
1791                let id = self.val(r, 0)?;
1792                if id >= 9997 {
1793                    if id == 9999 {
1794                        self.zones = vec![None; 48];
1795                    }
1796                    return Ok(());
1797                }
1798                let (a, b, len) = (self.point(r)?, self.point(r)?, self.val(r, 0)?);
1799                let mut text = Vec::new();
1800                for _ in 0..len.clamp(0, 80) {
1801                    text.push(r.next()?);
1802                }
1803                r.next()?;
1804                if let Some(z) = self.zones.get_mut(id.clamp(0, 47) as usize) {
1805                    *z = Some((a.0.min(b.0), a.1.min(b.1), a.0.max(b.0), a.1.max(b.1), text));
1806                }
1807            }
1808            5 => {
1809                if matches!(self.val(r, 0)?, 2 | 3) {
1810                    self.val(r, 0)?;
1811                    self.val(r, 0)?;
1812                }
1813            }
1814            6 => {
1815                self.val(r, 0)?;
1816            }
1817            // A user fill pattern: 16 rows of 16 characters, X for a bit.
1818            7 => {
1819                let slot = self.val(r, 0)?.clamp(0, 7) as usize;
1820                let mut rows = [0u16; 16];
1821                for row in &mut rows {
1822                    let text = r.text()?;
1823                    *row =
1824                        text.iter().take(16).enumerate().fold(0, |acc, (i, &c)| {
1825                            if c.eq_ignore_ascii_case(&b'X') { acc | 0x8000 >> i } else { acc }
1826                        });
1827                }
1828                self.user_patterns[slot] = rows;
1829            }
1830            // Rotating the pens' colors, a step at a time (see `cycle`);
1831            // 1,1,1,1 keeps the palette, and 0 times puts it back.
1832            8 => {
1833                let (a, b, times, delay) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1834                if (a, b, times, delay) == (1, 1, 1, 1) {
1835                    self.kept_palette = self.regs;
1836                }
1837                let clamp = |v: i32| if (0..=15).contains(&v) { v as u8 } else { 15 };
1838                let (a, b) = (clamp(a), clamp(b));
1839                if times == 0 {
1840                    self.regs = self.kept_palette;
1841                } else if a != b {
1842                    // Each step waits a screen refresh a pen, then the delay
1843                    // (200ths of a second), as IG does.
1844                    let step =
1845                        Duration::from_millis(((a.abs_diff(b) as u64 + 2) * 1000 / 60) + delay.max(0) as u64 * 5);
1846                    self.cycle = Some(Cycle { from: a, to: b, steps: times as u32 + 1, step });
1847                    self.cycle_step();
1848                }
1849            }
1850            // Store IG commands to run later.
1851            9 => match self.val(r, 0)? {
1852                0 => {
1853                    let mut stored = Vec::new();
1854                    while !stored.ends_with(b"||}") {
1855                        stored.push(r.next()?);
1856                    }
1857                    stored.truncate(stored.len() - 3);
1858                    self.stored = stored;
1859                }
1860                1 => self.run_stored = Some(self.stored.clone()),
1861                _ => self.stored.clear(),
1862            },
1863            10 => self.pen_at = self.point(r)?,
1864            _ => {}
1865        }
1866        Ok(())
1867    }
1868
1869    /// A click at a point of the bitmap: what to send the BBS, if a zone
1870    /// was being waited for.
1871    pub fn click(&mut self, x: i32, y: i32) -> Option<Vec<u8>> {
1872        let (cr, send) = self.awaiting_zone?;
1873        let hit = self.zones[..47]
1874            .iter()
1875            .flatten()
1876            .find(|z| x >= z.0 && x <= z.2 && y >= z.1 && y <= z.3)
1877            .or(self.zones[47].as_ref())?;
1878        let mut out = if send { hit.4.clone() } else { Vec::new() };
1879        self.awaiting_zone = None;
1880        if cr && send {
1881            out.push(b'\r');
1882        }
1883        Some(out)
1884    }
1885}
1886
1887#[cfg(test)]
1888mod tests {
1889    use super::*;
1890
1891    fn igs(data: &[u8]) -> Igs {
1892        let mut g = Igs::default();
1893        g.feed(data);
1894        g
1895    }
1896
1897    fn at(g: &Igs, x: usize, y: usize) -> u8 {
1898        g.pixels[y * g.width() + x]
1899    }
1900
1901    #[test]
1902    fn numbers_as_ig_reads_them() {
1903        let mut r = Reader { data: b"12,7:_\r\n34,1\r\n5\r\n2,x,+10,-3,!99,r,12345,", pos: 0 };
1904        let toks: Vec<Tok> = (0..9).map(|_| r.token().unwrap()).collect();
1905        assert_eq!(
1906            toks,
1907            [
1908                Tok::Num(12),
1909                Tok::Num(7),
1910                Tok::Num(34),
1911                Tok::Num(152),
1912                Tok::X,
1913                Tok::Plus(10),
1914                Tok::Minus(3),
1915                Tok::Bang(99),
1916                Tok::Rand,
1917            ]
1918        );
1919        assert_eq!(r.token().unwrap(), Tok::Num(0), "five digits are too many");
1920        assert!(Reader { data: b"12", pos: 0 }.token().is_err());
1921    }
1922
1923    #[test]
1924    fn lines_boxes_and_chains() {
1925        // A solid black box (pen 1 is register 3 in medium resolution),
1926        // then a line in red (pen 2, register 1), chained; then text.
1927        let g = igs(b"G#A>1,1,0:B>10,10,20,20,0:C>1,2:L 0,50,100,50:ok");
1928        assert_eq!(at(&g, 15, 15), 3);
1929        assert_eq!(at(&g, 25, 15), 0);
1930        assert_eq!(at(&g, 50, 50), 1);
1931        // The text after the chain is text: "ok" at the cursor, in black.
1932        assert!((0..8).any(|y| (0..16).any(|x| at(&g, x, y) == 3)));
1933    }
1934
1935    #[test]
1936    fn commands_split_across_reads() {
1937        let mut g = Igs::default();
1938        g.feed(b"G#C>2,2:B>0,0,");
1939        assert_eq!(at(&g, 5, 5), 0);
1940        g.feed(b"10,10,0:");
1941        assert_eq!(at(&g, 5, 5), 1);
1942    }
1943
1944    #[test]
1945    fn loops_step_and_read_data() {
1946        // Vertical lines at x = 0, 10, 20, 30 from a loop.
1947        let g = igs(b"G#C>1,1:&>0,30,10,0,L,4,x,0,x,9:");
1948        for x in [0, 10, 20, 30] {
1949            assert_eq!(at(&g, x, 5), 3, "x={x}");
1950        }
1951        assert_eq!(at(&g, 5, 5), 0);
1952        assert_eq!(at(&g, 40, 5), 0);
1953        // READ DATA: the same command with constant parameters, twice.
1954        let g = igs(b"G#C>1,1:&>0,0,1,0,L,8,100,0,100,9,200,0,200,9:");
1955        assert_eq!((at(&g, 100, 5), at(&g, 200, 5)), (3, 3));
1956    }
1957
1958    #[test]
1959    fn flood_fill_and_patterns() {
1960        // An outlined box, then flood its inside with pattern 4 (50%).
1961        let g = igs(b"G#A>0,1,1:B>0,0,40,20,0:A>2,4,0:F>10,10:");
1962        assert_eq!(at(&g, 0, 0), 3, "the border");
1963        let inside: Vec<u8> = (8..12).map(|x| at(&g, x, 10)).collect();
1964        assert!(inside.contains(&3) && inside.contains(&0), "{inside:?}");
1965        assert_eq!(at(&g, 50, 10), 0, "outside");
1966    }
1967
1968    #[test]
1969    fn vt52_text_and_queries() {
1970        let mut g = igs(b"\x1bY\x22\x25X\x1bb\x01Y");
1971        // "X" at column 5, row 2, black; "Y" after it in red.
1972        assert!((16..24).any(|y| (40..48).any(|x| at(&g, x, y) == 3)));
1973        assert!((16..24).any(|y| (48..56).any(|x| at(&g, x, y) == 1)));
1974        g.feed(b"G#?>0:?>3:");
1975        assert_eq!(g.replies, b"2.191:");
1976        // Low resolution: sixteen colors.
1977        g.feed(b"G#R>0,0:C>2,15:Z>0,0,9,9:");
1978        assert_eq!((g.width(), at(&g, 5, 5)), (320, 13));
1979    }
1980
1981    #[test]
1982    fn mouse_zones() {
1983        let mut g = igs(b"G#X>4,1,0,0,99,99,3,f/L:X>4,47,0,0,0,0,1,Q:<>1,2,1:");
1984        assert_eq!(g.click(50, 50), Some(b"f/L\r".to_vec()));
1985        assert_eq!(g.click(50, 50), None, "only while waiting");
1986        g.feed(b"G#< 0,2,1:");
1987        assert_eq!(g.click(300, 150), Some(b"Q".to_vec()));
1988    }
1989
1990    #[test]
1991    fn color_cycling_steps_over_time() {
1992        // Low resolution; keep the palette, then rotate pens 1-3 twice.
1993        let mut g = igs(b"G#R>0,0:X>8,1,1,1,1:X>8,1,3,1,0:C>2,1:Z>0,0,9,9:");
1994        let pens = |g: &Igs| [1u8, 2, 3].map(|p| g.regs[g.res.register(p) as usize]);
1995        let start = [DESKTOP[15], DESKTOP[1], DESKTOP[2]];
1996        // One step taken at once: each pen has the one before's color.
1997        assert_eq!(pens(&g), [start[2], start[0], start[1]]);
1998        assert_eq!(at(&g, 5, 5), 0, "the rest waits for the cycle");
1999        g.poll(Instant::now() + Duration::from_secs(1));
2000        assert_eq!(pens(&g), [start[1], start[2], start[0]]);
2001        assert_eq!(at(&g, 5, 5), 15, "then carries on");
2002        // 0 times puts the kept palette back.
2003        g.feed(b"G#X>8,1,3,0,0:");
2004        assert_eq!(pens(&g), start);
2005    }
2006
2007    #[test]
2008    fn pauses_hold_the_stream() {
2009        let mut g = igs(b"G#t>1:C>2,2:Z>0,0,9,9:");
2010        assert!(g.hold.is_some());
2011        assert_eq!(at(&g, 5, 5), 0);
2012        g.poll(Instant::now() + Duration::from_secs(2));
2013        assert_eq!(at(&g, 5, 5), 1);
2014    }
2015}