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 mouse zone: its rectangle (x0, y0, x1, y1) and what it sends.
363type Zone = (i32, i32, i32, i32, Vec<u8>);
364
365/// What the VT-52 escape parser is waiting for.
366#[derive(Clone, Copy, Debug, PartialEq, Eq)]
367enum Esc {
368    None,
369    Start,
370    Row,
371    Col(u8),
372    Fg,
373    Bg,
374}
375
376#[derive(Clone, Copy, Debug, PartialEq, Eq)]
377enum Mode {
378    Text,
379    /// After `G#`, or a `>`: a command letter comes next.
380    Command,
381}
382
383pub struct Igs {
384    pub res: Res,
385    pixels: Vec<u8>,
386    /// The blit buffer, screen sized.
387    memory: Vec<u8>,
388    memory_rect: (i32, i32, i32, i32),
389    regs: [StColor; 16],
390    attrs: Attrs,
391    user_patterns: [[u16; 16]; 8],
392    /// Where `D` draws from.
393    pen_at: (i32, i32),
394    hollow: bool,
395    scaling: i32,
396    rand: (i32, i32),
397    rand_big: (i32, i32),
398    seed: u32,
399    // VT-52.
400    col: usize,
401    row: usize,
402    fg: u8,
403    bg: u8,
404    reverse: bool,
405    wrap: bool,
406    pub cursor_on: bool,
407    destructive_bs: bool,
408    saved: (usize, usize),
409    esc: Esc,
410    // Parsing.
411    mode: Mode,
412    /// A `G` that may start `G#`.
413    g: bool,
414    buf: Vec<u8>,
415    /// IG commands stored by `X 9`, and when they're to run.
416    stored: Vec<u8>,
417    run_stored: Option<Vec<u8>>,
418    /// `t` and `q`: read nothing more until then.
419    pub hold: Option<Instant>,
420    looping: Option<Loop>,
421    last_text: Vec<u8>,
422    // The mouse.
423    zones: Vec<Option<Zone>>,
424    /// `<` with a mouse zone: (send a CR after, transmit it).
425    awaiting_zone: Option<(bool, bool)>,
426    /// What the BBS should get (replies, zones clicked).
427    pub replies: Vec<u8>,
428    pub bells: usize,
429}
430
431impl Default for Igs {
432    fn default() -> Igs {
433        let mut igs = Igs {
434            res: Res::Medium,
435            pixels: Vec::new(),
436            memory: Vec::new(),
437            memory_rect: (0, 0, 0, 0),
438            regs: DESKTOP,
439            attrs: Attrs::default(),
440            user_patterns: [ROM_UD_PATTERN; 8],
441            pen_at: (0, 0),
442            hollow: false,
443            scaling: 0,
444            rand: (0, 199),
445            rand_big: (0, 199),
446            seed: 0x1234_5678,
447            col: 0,
448            row: 0,
449            fg: 3,
450            bg: 0,
451            reverse: false,
452            wrap: true,
453            cursor_on: true,
454            destructive_bs: false,
455            saved: (0, 0),
456            esc: Esc::None,
457            mode: Mode::Text,
458            g: false,
459            buf: Vec::new(),
460            stored: Vec::new(),
461            run_stored: None,
462            hold: None,
463            looping: None,
464            last_text: Vec::new(),
465            zones: vec![None; 48],
466            awaiting_zone: None,
467            replies: Vec::new(),
468            bells: 0,
469        };
470        igs.set_res(Res::Medium);
471        igs.desktop_palette();
472        igs
473    }
474}
475
476impl Igs {
477    pub fn width(&self) -> usize {
478        self.res.size().0
479    }
480
481    pub fn height(&self) -> usize {
482        self.res.size().1
483    }
484
485    /// The bitmap: a color register a pixel.
486    pub fn pixels(&self) -> &[u8] {
487        &self.pixels
488    }
489
490    pub fn palette(&self) -> [Rgb; 16] {
491        std::array::from_fn(|i| rgb(self.regs[i]))
492    }
493
494    /// Where the VT-52 cursor is, on the front end's 80x25 text screen.
495    pub fn cursor(&self) -> (usize, usize) {
496        match self.res {
497            Res::Low => (self.col * 2, self.row),
498            Res::Medium => (self.col, self.row),
499            Res::High => (self.col, self.row / 2),
500        }
501    }
502
503    fn text_size(&self) -> (usize, usize) {
504        (self.width() / 8, self.height() / self.res.cell_h())
505    }
506
507    fn set_res(&mut self, res: Res) {
508        self.res = res;
509        let (w, h) = res.size();
510        self.pixels = vec![0; w * h];
511        self.memory = vec![0; w * h];
512        let (cols, rows) = self.text_size();
513        self.col = self.col.min(cols - 1);
514        self.row = self.row.min(rows - 1);
515        let mask = res.colors() - 1;
516        self.fg &= mask;
517        self.bg &= mask;
518        if res == Res::Medium {
519            self.fg = 3;
520        }
521    }
522
523    fn desktop_palette(&mut self) {
524        self.regs = DESKTOP;
525        match self.res {
526            Res::Medium => self.regs[..4].copy_from_slice(&DESKTOP_MEDIUM),
527            Res::High => self.regs[..2].copy_from_slice(&[0x777, 0x000]),
528            Res::Low => {}
529        }
530    }
531
532    fn random(&mut self, (lo, hi): (i32, i32)) -> i32 {
533        self.seed ^= self.seed << 13;
534        self.seed ^= self.seed >> 17;
535        self.seed ^= self.seed << 5;
536        lo + (self.seed % ((hi - lo).max(0) as u32 + 1)) as i32
537    }
538
539    /// Bytes from the BBS.
540    pub fn feed(&mut self, data: &[u8]) {
541        self.buf.extend_from_slice(data);
542        let mut buf = std::mem::take(&mut self.buf);
543        let mut pos = 0;
544        while pos < buf.len() && self.hold.is_none() {
545            match self.mode {
546                Mode::Text => {
547                    let b = buf[pos];
548                    pos += 1;
549                    self.text_byte(b);
550                }
551                Mode::Command => {
552                    let mut r = Reader { data: &buf, pos };
553                    match self.command(&mut r) {
554                        Ok(chain) => {
555                            pos = r.pos;
556                            self.mode = if chain { Mode::Command } else { Mode::Text };
557                            // X 9: run the stored commands next.
558                            if let Some(run) = self.run_stored.take() {
559                                buf.splice(pos..pos, run);
560                            }
561                        }
562                        Err(Incomplete) => {
563                            self.looping = None;
564                            break;
565                        }
566                    }
567                }
568            }
569        }
570        buf.drain(..pos);
571        // A command that never ends shouldn't swallow everything after it.
572        if buf.len() > 1 << 20 {
573            buf.clear();
574            self.mode = Mode::Text;
575        }
576        self.buf = buf;
577    }
578
579    /// Carry on after a pause (`t`, `q`), once its time has come.
580    pub fn poll(&mut self, now: Instant) {
581        if self.hold.is_some_and(|t| now >= t) {
582            self.hold = None;
583            self.feed(&[]);
584        }
585    }
586
587    // ---- VT-52 text ----
588
589    fn text_byte(&mut self, b: u8) {
590        if self.g {
591            self.g = false;
592            if b == b'#' && self.esc == Esc::None {
593                self.mode = Mode::Command;
594                return;
595            }
596            self.vt52(b'G');
597        }
598        if b == b'G' && self.esc == Esc::None {
599            self.g = true;
600            return;
601        }
602        self.vt52(b);
603    }
604
605    fn vt52(&mut self, b: u8) {
606        let (cols, rows) = self.text_size();
607        match self.esc {
608            Esc::Start => {
609                self.esc = Esc::None;
610                self.vt52_escape(b);
611                return;
612            }
613            Esc::Row => {
614                self.esc = Esc::Col(b.saturating_sub(32));
615                return;
616            }
617            Esc::Col(r) => {
618                self.esc = Esc::None;
619                self.row = (r as usize).min(rows - 1);
620                self.col = (b.saturating_sub(32) as usize).min(cols - 1);
621                return;
622            }
623            Esc::Fg => {
624                self.esc = Esc::None;
625                self.fg = b & (self.res.colors() - 1);
626                return;
627            }
628            Esc::Bg => {
629                self.esc = Esc::None;
630                self.bg = b & (self.res.colors() - 1);
631                return;
632            }
633            Esc::None => {}
634        }
635        match b {
636            0x07 => self.bells += 1,
637            0x08 => {
638                self.col = self.col.saturating_sub(1);
639                if self.destructive_bs {
640                    self.glyph_cell(b' ');
641                }
642            }
643            0x09 => self.col = ((self.col / 8 + 1) * 8).min(cols - 1),
644            0x0A..=0x0C => self.line_feed(),
645            0x0D => self.col = 0,
646            0x1B => self.esc = Esc::Start,
647            0x00..=0x1F => {}
648            _ => {
649                if self.col >= cols {
650                    if !self.wrap {
651                        self.col = cols - 1;
652                    } else {
653                        self.col = 0;
654                        self.line_feed();
655                    }
656                }
657                self.glyph_cell(b);
658                self.col += 1;
659                if self.col >= cols && !self.wrap {
660                    self.col = cols - 1;
661                }
662            }
663        }
664    }
665
666    fn vt52_escape(&mut self, b: u8) {
667        let (cols, rows) = self.text_size();
668        match b {
669            b'A' => self.row = self.row.saturating_sub(1),
670            b'B' => self.row = (self.row + 1).min(rows - 1),
671            b'C' => self.col = (self.col + 1).min(cols - 1),
672            b'D' => self.col = self.col.saturating_sub(1),
673            b'E' => {
674                self.clear_text_rows(0, rows);
675                (self.col, self.row) = (0, 0);
676            }
677            b'H' => (self.col, self.row) = (0, 0),
678            b'I' => {
679                if self.row == 0 {
680                    self.scroll_text(0, false);
681                } else {
682                    self.row -= 1;
683                }
684            }
685            b'J' => {
686                self.clear_line_from(self.col);
687                self.clear_text_rows(self.row + 1, rows);
688            }
689            b'K' => self.clear_line_from(self.col),
690            b'L' => {
691                self.scroll_text(self.row, false);
692                self.col = 0;
693            }
694            b'M' => {
695                self.scroll_text(self.row, true);
696                self.col = 0;
697            }
698            b'Y' => self.esc = Esc::Row,
699            b'b' => self.esc = Esc::Fg,
700            b'c' => self.esc = Esc::Bg,
701            b'd' => {
702                self.clear_text_rows(0, self.row);
703                self.clear_line_to(self.col);
704            }
705            b'e' => self.cursor_on = true,
706            b'f' => self.cursor_on = false,
707            b'j' => self.saved = (self.col, self.row),
708            b'k' => (self.col, self.row) = self.saved,
709            b'l' => self.clear_text_rows(self.row, self.row + 1),
710            b'o' => self.clear_line_to(self.col),
711            b'p' => self.reverse = true,
712            b'q' => self.reverse = false,
713            b'v' => self.wrap = true,
714            b'w' => self.wrap = false,
715            _ => {}
716        }
717    }
718
719    fn line_feed(&mut self) {
720        let rows = self.text_size().1;
721        if self.row + 1 >= rows {
722            self.scroll_text(0, true);
723        } else {
724            self.row += 1;
725        }
726    }
727
728    /// Scroll the text rows from `top` down: up a row (`up`), or down,
729    /// leaving a blank row.
730    fn scroll_text(&mut self, top: usize, up: bool) {
731        let (w, h) = self.res.size();
732        let ch = self.res.cell_h();
733        let (start, end) = (top * ch * w, h * w);
734        if start >= end {
735            return;
736        }
737        if up {
738            self.pixels.copy_within(start + ch * w..end, start);
739            self.pixels[end - ch * w..end].fill(self.bg);
740        } else {
741            self.pixels.copy_within(start..end - ch * w, start + ch * w);
742            self.pixels[start..start + ch * w].fill(self.bg);
743        }
744    }
745
746    fn clear_text_rows(&mut self, from: usize, to: usize) {
747        let w = self.width();
748        let ch = self.res.cell_h();
749        let (a, b) = ((from * ch * w).min(self.pixels.len()), (to * ch * w).min(self.pixels.len()));
750        if a < b {
751            self.pixels[a..b].fill(self.bg);
752        }
753    }
754
755    fn clear_cells(&mut self, row: usize, from: usize, to: usize) {
756        let w = self.width();
757        let ch = self.res.cell_h();
758        for y in row * ch..(row + 1) * ch {
759            let line = &mut self.pixels[y * w..(y + 1) * w];
760            line[(from * 8).min(w)..(to * 8).min(w)].fill(self.bg);
761        }
762    }
763
764    fn clear_line_from(&mut self, col: usize) {
765        let cols = self.text_size().0;
766        self.clear_cells(self.row, col, cols);
767    }
768
769    fn clear_line_to(&mut self, col: usize) {
770        self.clear_cells(self.row, 0, col + 1);
771    }
772
773    /// A character at the cursor, in the text colors.
774    fn glyph_cell(&mut self, ch: u8) {
775        let w = self.width();
776        let h = self.res.cell_h();
777        let (fg, bg) = if self.reverse { (self.bg, self.fg) } else { (self.fg, self.bg) };
778        let (x0, y0) = (self.col * 8, self.row * h);
779        for gy in 0..h {
780            let bits = glyph_row(ch, gy, h);
781            let row = &mut self.pixels[(y0 + gy) * w + x0..][..8];
782            for (gx, p) in row.iter_mut().enumerate() {
783                *p = if bits & (0x80 >> gx) != 0 { fg } else { bg };
784            }
785        }
786    }
787
788    // ---- Drawing ----
789
790    fn mask(&self) -> u8 {
791        self.res.colors() - 1
792    }
793
794    /// A pixel in the writing mode: `on` is the pattern's bit there.
795    fn plot(&mut self, x: i32, y: i32, on: bool, color: u8, mode: u8) {
796        let (w, h) = self.res.size();
797        if x < 0 || y < 0 || x as usize >= w || y as usize >= h {
798            return;
799        }
800        let p = &mut self.pixels[y as usize * w + x as usize];
801        match mode {
802            2 if on => *p = color,
803            3 if on => *p ^= self.res.colors() - 1,
804            4 if !on => *p = color,
805            1 => *p = if on { color } else { 0 },
806            _ => {}
807        }
808    }
809
810    fn reg(&self, pen: u8) -> u8 {
811        self.res.register(pen)
812    }
813
814    /// The fill pattern's bit at a pixel.
815    fn pattern_bit(&self, x: i32, y: i32) -> bool {
816        let f = self.attrs.fill;
817        let fi = f.style.max(1) as usize - 1;
818        let word = match f.interior {
819            0 => return false,
820            1 => return true,
821            2 if fi < 8 => DITHER[fi * 4 + (y & 3) as usize],
822            2 => OEMPAT[((fi - 8).min(15)) * 8 + (y & 7) as usize],
823            3 if fi < 6 => HATCH0[fi * 8 + (y & 7) as usize],
824            3 => HATCH1[((fi - 6).min(5)) * 16 + (y & 15) as usize],
825            _ => self.user_patterns[fi.min(7)][(y & 15) as usize],
826        };
827        word & (0x8000 >> (x & 15)) != 0
828    }
829
830    /// A horizontal span of the fill.
831    fn fill_span(&mut self, x0: i32, x1: i32, y: i32) {
832        if self.attrs.fill.interior == 0 {
833            return;
834        }
835        let color = self.reg(self.attrs.fill.pen);
836        let mode = self.attrs.mode;
837        for x in x0.max(0)..=x1.min(self.width() as i32 - 1) {
838            let on = self.pattern_bit(x, y);
839            self.plot(x, y, on, color, mode);
840        }
841    }
842
843    /// A filled polygon (even-odd), and its outline if the perimeter is on.
844    fn polygon(&mut self, pts: &[(i32, i32)], perimeter: bool) {
845        if pts.is_empty() {
846            return;
847        }
848        let (ymin, ymax) = pts.iter().fold((i32::MAX, i32::MIN), |(a, b), p| (a.min(p.1), b.max(p.1)));
849        let h = self.height() as i32;
850        for y in ymin.max(0)..=ymax.min(h - 1) {
851            let mut xs: Vec<i32> = Vec::new();
852            for i in 0..pts.len() {
853                let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
854                let (lo, hi) = if a.1 < b.1 { (a, b) } else { (b, a) };
855                if y >= lo.1 && y < hi.1 || y == ymax && hi.1 == ymax && lo.1 != hi.1 {
856                    let t = (y - lo.1) as f64 / (hi.1 - lo.1) as f64;
857                    xs.push((lo.0 as f64 + t * (hi.0 - lo.0) as f64).round() as i32);
858                }
859            }
860            xs.sort_unstable();
861            for pair in xs.chunks(2) {
862                if let [a, b] = pair {
863                    self.fill_span(*a, *b, y);
864                }
865            }
866        }
867        if perimeter {
868            let color = self.reg(self.attrs.fill.pen);
869            let mut closed = pts.to_vec();
870            closed.push(pts[0]);
871            for seg in closed.windows(2) {
872                self.thin_line(seg[0], seg[1], color, 0xFFFF, self.attrs.mode);
873            }
874        }
875    }
876
877    /// A one-pixel line in a style.
878    fn thin_line(&mut self, (x0, y0): (i32, i32), (x1, y1): (i32, i32), color: u8, style: u16, mode: u8) {
879        let (dx, dy) = ((x1 - x0).abs(), -(y1 - y0).abs());
880        let (sx, sy) = (if x0 < x1 { 1 } else { -1 }, if y0 < y1 { 1 } else { -1 });
881        let (mut x, mut y, mut err) = (x0, y0, dx + dy);
882        let mut bit = 0u32;
883        loop {
884            let on = style.rotate_left(bit) & 0x8000 != 0;
885            // Lines draw their pattern's zero bits only when replacing.
886            if on || mode == 1 || mode == 4 {
887                self.plot(x, y, on, color, mode);
888            }
889            bit += 1;
890            if x == x1 && y == y1 {
891                break;
892            }
893            let e2 = 2 * err;
894            if e2 >= dy {
895                err += dy;
896                x += sx;
897            }
898            if e2 <= dx {
899                err += dx;
900                y += sy;
901            }
902        }
903    }
904
905    /// A line in the line attributes: wide lines are filled polygons,
906    /// with square, round or arrow ends.
907    fn line(&mut self, a: (i32, i32), b: (i32, i32)) {
908        let l = self.attrs.line;
909        let color = self.reg(l.pen);
910        let style = match l.style {
911            1..=6 => LINE_STYLES[l.style as usize - 1],
912            _ => {
913                let p = self.user_patterns[6];
914                p[0]
915            }
916        };
917        if l.width <= 1 {
918            self.thin_line(a, b, color, style, self.attrs.mode);
919            self.line_ends(a, b, color, 1);
920            return;
921        }
922        let saved = self.attrs.fill;
923        self.attrs.fill = FillAttr { pen: l.pen, interior: 1, style: 1, perimeter: false };
924        let (dx, dy) = ((b.0 - a.0) as f64, (b.1 - a.1) as f64);
925        let len = (dx * dx + dy * dy).sqrt().max(1.0);
926        let half = l.width as f64 / 2.0;
927        // Perpendicular, with y squashed for the screen's pixel shape.
928        let (px, py) = (-dy / len * half, dx / len * half * self.res.aspect());
929        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);
930        self.polygon(&[q(a, 1.0), q(b, 1.0), q(b, -1.0), q(a, -1.0)], false);
931        self.line_ends(a, b, color, l.width);
932        self.attrs.fill = saved;
933    }
934
935    fn line_ends(&mut self, a: (i32, i32), b: (i32, i32), color: u8, width: i32) {
936        let ends = self.attrs.line.ends;
937        for (end, (at, from)) in [(ends.0, (a, b)), (ends.1, (b, a))] {
938            match end {
939                1 => self.arrow(at, from, color, width),
940                2 if width > 1 => {
941                    let r = width / 2;
942                    let saved = self.attrs.fill;
943                    self.attrs.fill = FillAttr { pen: self.attrs.line.pen, interior: 1, style: 1, perimeter: false };
944                    let pts = self.ellipse_points(at, r, (r as f64 * self.res.aspect()) as i32, 0, 3600);
945                    self.polygon(&pts, false);
946                    self.attrs.fill = saved;
947                }
948                _ => {}
949            }
950        }
951    }
952
953    fn arrow(&mut self, tip: (i32, i32), from: (i32, i32), _color: u8, width: i32) {
954        let (dx, dy) = ((tip.0 - from.0) as f64, (tip.1 - from.1) as f64);
955        let len = (dx * dx + dy * dy).sqrt().max(1.0);
956        let size = (8 + 2 * width) as f64;
957        let (ux, uy) = (dx / len, dy / len);
958        let back = (tip.0 as f64 - ux * size, tip.1 as f64 - uy * size);
959        let side = size / 2.0;
960        let p1 = ((back.0 - uy * side) as i32, (back.1 + ux * side * self.res.aspect()) as i32);
961        let p2 = ((back.0 + uy * side) as i32, (back.1 - ux * side * self.res.aspect()) as i32);
962        let saved = self.attrs.fill;
963        self.attrs.fill = FillAttr { pen: self.attrs.line.pen, interior: 1, style: 1, perimeter: false };
964        self.polygon(&[tip, p1, p2], false);
965        self.attrs.fill = saved;
966    }
967
968    /// Points around an ellipse from `start` to `end`, tenths of a degree
969    /// counter-clockwise from three o'clock (the VDI's way).
970    fn ellipse_points(&self, c: (i32, i32), rx: i32, ry: i32, start: i32, end: i32) -> Vec<(i32, i32)> {
971        let mut span = end - start;
972        if span <= 0 {
973            span += 3600;
974        }
975        let steps = (rx.max(ry) / 4).clamp(32, 128);
976        (0..=steps)
977            .map(|i| {
978                let a = (start as f64 + span as f64 * i as f64 / steps as f64).to_radians() / 10.0;
979                ((c.0 as f64 + rx as f64 * a.cos()).round() as i32, (c.1 as f64 - ry as f64 * a.sin()).round() as i32)
980            })
981            .collect()
982    }
983
984    fn polyline(&mut self, pts: &[(i32, i32)]) {
985        for seg in pts.windows(2) {
986            self.line(seg[0], seg[1]);
987        }
988    }
989
990    /// A filled shape: interior, then its outline if the perimeter's on.
991    fn shape(&mut self, pts: &[(i32, i32)]) {
992        self.polygon(pts, self.attrs.fill.perimeter);
993    }
994
995    fn rounded_box(&mut self, (x0, y0, x1, y1): (i32, i32, i32, i32), filled: bool) {
996        let (x0, x1) = (x0.min(x1), x0.max(x1));
997        let (y0, y1) = (y0.min(y1), y0.max(y1));
998        let rx = ((x1 - x0) / 2).min(8);
999        let ry = (((y1 - y0) / 2) as f64).min(8.0 * self.res.aspect()) as i32;
1000        let mut pts = Vec::new();
1001        for (cx, cy, a0) in
1002            [(x1 - rx, y0 + ry, 0), (x0 + rx, y0 + ry, 900), (x0 + rx, y1 - ry, 1800), (x1 - rx, y1 - ry, 2700)]
1003        {
1004            pts.extend(self.ellipse_points((cx, cy), rx, ry, a0, a0 + 900));
1005        }
1006        if filled {
1007            self.shape(&pts);
1008        } else {
1009            pts.push(pts[0]);
1010            self.polyline(&pts);
1011        }
1012    }
1013
1014    /// Fill the area of one color around a point (`F`).
1015    fn flood(&mut self, x: i32, y: i32) {
1016        let (w, h) = self.res.size();
1017        if x < 0 || y < 0 || x as usize >= w || y as usize >= h {
1018            return;
1019        }
1020        let target = self.pixels[y as usize * w + x as usize];
1021        let mut seen = vec![false; w * h];
1022        let mut stack = vec![(x as usize, y as usize)];
1023        let mut spans = Vec::new();
1024        while let Some((x, y)) = stack.pop() {
1025            if seen[y * w + x] || self.pixels[y * w + x] != target {
1026                continue;
1027            }
1028            let (mut a, mut b) = (x, x);
1029            while a > 0 && !seen[y * w + a - 1] && self.pixels[y * w + a - 1] == target {
1030                a -= 1;
1031            }
1032            while b + 1 < w && !seen[y * w + b + 1] && self.pixels[y * w + b + 1] == target {
1033                b += 1;
1034            }
1035            for i in a..=b {
1036                seen[y * w + i] = true;
1037                for ny in [y.wrapping_sub(1), y + 1] {
1038                    if ny < h && !seen[ny * w + i] && self.pixels[ny * w + i] == target {
1039                        stack.push((i, ny));
1040                    }
1041                }
1042            }
1043            spans.push((a as i32, b as i32, y as i32));
1044        }
1045        for (a, b, y) in spans {
1046            self.fill_span(a, b, y);
1047        }
1048    }
1049
1050    fn marker(&mut self, x: i32, y: i32) {
1051        let (pen, kind, size) = self.attrs.marker;
1052        let color = self.reg(pen);
1053        let shape = MARKERS[(kind.clamp(1, 6) - 1) as usize];
1054        let scale = if kind == 1 { 0.0 } else { size.max(1) as f64 };
1055        for poly in shape {
1056            let pts: Vec<(i32, i32)> =
1057                poly.iter().map(|&(dx, dy)| (x + (dx as f64 * scale) as i32, y + (dy as f64 * scale) as i32)).collect();
1058            for seg in pts.windows(2) {
1059                self.thin_line(seg[0], seg[1], color, 0xFFFF, self.attrs.mode);
1060            }
1061        }
1062    }
1063
1064    /// Text at a point (its baseline), as `W` writes it.
1065    fn write_text(&mut self, x: i32, y: i32, text: &[u8]) {
1066        let t = self.attrs.text;
1067        let color = self.reg(t.pen);
1068        // The system font's sizes, as the ST has them: 6x6 at 8 points,
1069        // 8x8 at 9, 8x16 at 10, and twice those at 16, 18 and 20.
1070        let (cell, scale) = match t.points {
1071            ..=8 => (6, 1),
1072            9 => (8, 1),
1073            10..=15 => (16, 1),
1074            16..=17 => (6, 2),
1075            18..=19 => (8, 2),
1076            _ => (16, 2),
1077        };
1078        let ascent = match cell {
1079            6 => 4,
1080            8 => 6,
1081            _ => 13,
1082        } * scale;
1083        let (cw, ch) = (cell.min(8) * scale, cell * scale);
1084        for (i, &c) in text.iter().enumerate() {
1085            for gy in 0..ch {
1086                let mut bits = glyph_row(c, (gy / scale) as usize, cell as usize) as u32;
1087                if t.effects & 1 != 0 {
1088                    bits |= bits >> 1;
1089                }
1090                if t.effects & 2 != 0 && gy % 2 == 1 {
1091                    bits &= 0x55;
1092                }
1093                if t.effects & 8 != 0 && gy == ascent + scale {
1094                    bits = 0xFF;
1095                }
1096                let skew = if t.effects & 4 != 0 { (ch - gy) / 4 } else { 0 };
1097                for gx in 0..cw {
1098                    let on = bits & (0x80 >> (gx / scale)) != 0;
1099                    if !on && self.attrs.mode != 1 {
1100                        continue;
1101                    }
1102                    // Along the text and down from its top, turned.
1103                    let (ax, ay) = ((i as i32) * cw + gx + skew, gy - ascent);
1104                    let (px, py) = match t.rotation % 4 {
1105                        1 => (x + ay, y - ax),
1106                        2 => (x - ax, y - ay),
1107                        3 => (x - ay, y + ax),
1108                        _ => (x + ax, y + ay),
1109                    };
1110                    self.plot(px, py, on, color, self.attrs.mode);
1111                }
1112            }
1113        }
1114    }
1115
1116    /// `G`: copy pixels between the screen and the blit buffer, combining
1117    /// them with one of the 16 logic operations.
1118    fn blit(&mut self, from_memory: bool, src: (i32, i32, i32, i32), to_memory: bool, dst: (i32, i32), op: u8) {
1119        let (w, h) = self.res.size();
1120        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));
1121        let source: Vec<u8> = if from_memory { self.memory.clone() } else { self.pixels.clone() };
1122        let mask = self.mask();
1123        for y in 0..=(y1 - y0) {
1124            for x in 0..=(x1 - x0) {
1125                let (sx, sy, dx, dy) = (x0 + x, y0 + y, dst.0 + x, dst.1 + y);
1126                if sx < 0 || sy < 0 || dx < 0 || dy < 0 {
1127                    continue;
1128                }
1129                let (sx, sy, dx, dy) = (sx as usize, sy as usize, dx as usize, dy as usize);
1130                if sx >= w || sy >= h || dx >= w || dy >= h {
1131                    continue;
1132                }
1133                let s = source[sy * w + sx];
1134                let target = if to_memory { &mut self.memory } else { &mut self.pixels };
1135                let d = target[dy * w + dx];
1136                let v = match op {
1137                    0 => 0,
1138                    1 => s & d,
1139                    2 => s & !d,
1140                    4 => !s & d,
1141                    5 => d,
1142                    6 => s ^ d,
1143                    7 => s | d,
1144                    8 => !(s | d),
1145                    9 => !(s ^ d),
1146                    10 => !d,
1147                    11 => s | !d,
1148                    12 => !s,
1149                    13 => !s | d,
1150                    14 => !(s & d),
1151                    15 => 0xFF,
1152                    _ => s,
1153                };
1154                target[dy * w + dx] = v & mask;
1155            }
1156        }
1157    }
1158
1159    fn clear_screen(&mut self) {
1160        self.pixels.fill(0);
1161    }
1162}
1163
1164/// A row of a glyph, 6, 8 or 16 high (CP437 at 8 and 16; ASCII at 6,
1165/// where anything else comes from the 8x8 font).
1166fn glyph_row(c: u8, y: usize, h: usize) -> u8 {
1167    match h {
1168        16 => FONT16[c as usize * 16 + y.min(15)],
1169        6 if (0x20..0x7F).contains(&c) => FONT6[c as usize * 6 + y.min(5)],
1170        6 => FONT8[c as usize * 8 + (y * 8 / 6).min(7)],
1171        _ => FONT8[c as usize * 8 + y.min(7)],
1172    }
1173}
1174
1175// The command interpreter, in its own impl for its size.
1176impl Igs {
1177    /// A number, from the stream or a running loop, scaled by `g` if it's
1178    /// a coordinate (`axis` 1 for x, 2 for y).
1179    fn val(&mut self, r: &mut Reader, axis: u8) -> Res2<i32> {
1180        let tok = match self.looping.as_mut() {
1181            Some(lp) if lp.running || lp.at != 0 => lp.next(),
1182            _ => r.token()?,
1183        };
1184        let mut n = match tok {
1185            Tok::Num(n) | Tok::Plus(n) | Tok::Bang(n) => n,
1186            Tok::Minus(n) => -n,
1187            Tok::Rand => self.random(self.rand),
1188            Tok::RandBig => self.random(self.rand_big),
1189            Tok::X | Tok::Y => 0,
1190        };
1191        if self.scaling == 1 && axis > 0 && n > 0 {
1192            let max = if axis == 1 { self.width() - 1 } else { self.height() - 1 } as f64;
1193            n = (n as f64 * max / 9999.0).round() as i32;
1194        } else if self.scaling == 2 && axis == 2 && self.res == Res::High {
1195            n *= 2;
1196        }
1197        Ok(n)
1198    }
1199
1200    fn point(&mut self, r: &mut Reader) -> Res2<(i32, i32)> {
1201        Ok((self.val(r, 1)?, self.val(r, 2)?))
1202    }
1203
1204    /// One command, its letter next in `r`. Returns whether a chained
1205    /// command follows.
1206    fn command(&mut self, r: &mut Reader) -> Res2<bool> {
1207        let mut key = r.next()?;
1208        // A new G# where a chained command was expected starts over.
1209        while key == b'G' && r.data.get(r.pos) == Some(&b'#') {
1210            r.pos += 1;
1211            key = r.next()?;
1212        }
1213        let chain = r.next()? == b'>';
1214        if key == b'&' {
1215            self.loop_command(r)?;
1216        } else {
1217            self.run(key, r)?;
1218        }
1219        Ok(chain)
1220    }
1221
1222    /// `&`: run a command (or a "chain gang" of them) many times.
1223    fn loop_command(&mut self, r: &mut Reader) -> Res2<()> {
1224        let (from, to, step, _delay) = (r.token()?, r.token()?, r.token()?, r.token()?);
1225        let num = |t: Tok| if let Tok::Num(n) = t { n } else { 0 };
1226        let (from, to, step) = (num(from), num(to), num(step));
1227        let mut step = if from > to { -step } else { step };
1228        if step == 0 {
1229            step = 1;
1230        }
1231        let key = r.next()?;
1232        let gang = if key == b'>' { Some(r.text()?) } else { None };
1233        let kind = r.next()?;
1234        let count = (num(r.token()?)).clamp(1, 2048) as usize;
1235        let mut params = Vec::with_capacity(count);
1236        for _ in 0..count {
1237            params.push(r.token()?);
1238        }
1239        let plain = !params.iter().any(|t| !matches!(t, Tok::Num(_) | Tok::Rand | Tok::RandBig));
1240        self.looping = Some(Loop {
1241            params,
1242            at: 0,
1243            value: from,
1244            to,
1245            step,
1246            from,
1247            kind,
1248            x_count: 0,
1249            first: 0,
1250            second: 0,
1251            was: from,
1252            plain,
1253            running: true,
1254        });
1255        let mut guard = 0;
1256        loop {
1257            let cmd = match &gang {
1258                Some(g) => {
1259                    let i = self.val(r, 0)?.clamp(0, 127) as usize;
1260                    g.get(i).copied().unwrap_or(b' ')
1261                }
1262                None => key,
1263            };
1264            // W reads its text from the stream each time only with `@`.
1265            self.run(cmd, r)?;
1266            guard += 1;
1267            let done = self.looping.as_ref().is_none_or(|lp| !lp.running && lp.at == 0);
1268            if done || guard > 100_000 {
1269                break;
1270            }
1271        }
1272        self.looping = None;
1273        Ok(())
1274    }
1275
1276    fn in_loop(&self) -> bool {
1277        self.looping.is_some()
1278    }
1279
1280    fn run(&mut self, key: u8, r: &mut Reader) -> Res2<()> {
1281        match key {
1282            b'A' => {
1283                let (t, p, b) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1284                let f = &mut self.attrs.fill;
1285                f.interior = if (0..=4).contains(&t) { t as u8 } else { 0 };
1286                if f.interior == 4 && p >= 8 {
1287                    // 8: random sand in slot 0; 9 and up: its default.
1288                    self.user_patterns[0] = if p == 8 {
1289                        std::array::from_fn(|_| {
1290                            self.seed ^= self.seed << 13;
1291                            self.seed ^= self.seed >> 17;
1292                            self.seed ^= self.seed << 5;
1293                            (self.seed & self.seed >> 7) as u16
1294                        })
1295                    } else {
1296                        ROM_UD_PATTERN
1297                    };
1298                    self.attrs.fill.style = 1;
1299                } else {
1300                    self.attrs.fill.style =
1301                        if self.attrs.fill.interior == 4 { (p + 1).clamp(1, 8) as u8 } else { p.clamp(1, 24) as u8 };
1302                }
1303                self.attrs.fill.perimeter = b != 0;
1304            }
1305            b'B' => {
1306                let (a, b, round) = (self.point(r)?, self.point(r)?, self.val(r, 0)?);
1307                if round != 0 {
1308                    self.rounded_box((a.0, a.1, b.0, b.1), true);
1309                } else {
1310                    self.shape(&[(a.0, a.1), (b.0, a.1), (b.0, b.1), (a.0, b.1)]);
1311                }
1312            }
1313            b'C' => {
1314                let (which, pen) = (self.val(r, 0)?, self.val(r, 0)?);
1315                let pen = if (0..=15).contains(&pen) { pen as u8 } else { 0 };
1316                match which {
1317                    0 => self.attrs.marker.0 = pen,
1318                    1 => self.attrs.line.pen = pen,
1319                    2 => self.attrs.fill.pen = pen,
1320                    3 => self.attrs.text.pen = pen,
1321                    _ => self.attrs.marker.0 = pen,
1322                }
1323            }
1324            b'D' => {
1325                let to = self.point(r)?;
1326                let from = self.pen_at;
1327                self.line(from, to);
1328                self.pen_at = to;
1329            }
1330            b'E' => {
1331                let (fx, size, rot) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1332                self.attrs.text.effects = fx as u8;
1333                self.attrs.text.points = size.clamp(8, 20);
1334                self.attrs.text.rotation = if (0..=4).contains(&rot) { rot as u8 } else { 0 };
1335            }
1336            b'F' => {
1337                let (x, y) = self.point(r)?;
1338                self.flood(x, y);
1339            }
1340            b'f' | b'z' => {
1341                let n = self.val(r, 0)?.clamp(0, 128) as usize;
1342                let mut pts = Vec::with_capacity(n);
1343                for _ in 0..n {
1344                    pts.push(self.point(r)?);
1345                }
1346                if key == b'f' {
1347                    self.shape(&pts);
1348                } else if n >= 2 {
1349                    self.polyline(&pts);
1350                }
1351            }
1352            b'G' => self.grab(r)?,
1353            b'g' => {
1354                let v = self.val(r, 0)?;
1355                self.scaling = if (0..=2).contains(&v) { v } else { 0 };
1356            }
1357            b'H' => {
1358                self.hollow = self.val(r, 0)? != 0;
1359                if self.hollow {
1360                    self.attrs.fill.interior = 0;
1361                    self.attrs.mode = 2;
1362                    self.attrs.fill.perimeter = true;
1363                } else {
1364                    self.attrs.fill.interior = 1;
1365                    self.attrs.mode = 1;
1366                    self.attrs.fill.perimeter = false;
1367                }
1368            }
1369            b'I' => match self.val(r, 0)? {
1370                1 => self.desktop_palette(),
1371                2 => self.attrs = Attrs::default(),
1372                3 => self.regs = IG_PALETTE,
1373                4 => self.desktop_palette(),
1374                5 => {
1375                    if self.res == Res::Low {
1376                        self.set_res(Res::Medium);
1377                    }
1378                }
1379                _ => {
1380                    self.desktop_palette();
1381                    self.attrs = Attrs::default();
1382                }
1383            },
1384            b'J' | b'Y' => {
1385                let (c, rx, ry) = (self.point(r)?, self.val(r, 1)?, self.val(r, 2)?);
1386                let (a0, a1) = (self.val(r, 0)? * 10, self.val(r, 0)? * 10);
1387                let pts = self.ellipse_points(c, rx, ry, a0, a1);
1388                if key == b'J' {
1389                    self.polyline(&pts);
1390                } else {
1391                    let mut pie = vec![c];
1392                    pie.extend(pts);
1393                    self.shape(&pie);
1394                }
1395            }
1396            b'K' | b'V' => {
1397                let (c, rad) = (self.point(r)?, self.val(r, 1)?);
1398                let (a0, a1) = (self.val(r, 0)? * 10, self.val(r, 0)? * 10);
1399                let ry = (rad as f64 * self.res.aspect()).round() as i32;
1400                let pts = self.ellipse_points(c, rad, ry, a0, a1);
1401                if key == b'K' {
1402                    self.polyline(&pts);
1403                } else {
1404                    let mut pie = vec![c];
1405                    pie.extend(pts);
1406                    self.shape(&pie);
1407                }
1408            }
1409            b'k' => match self.val(r, 0)? {
1410                0 => self.cursor_on = false,
1411                1 => self.cursor_on = true,
1412                2 => self.destructive_bs = true,
1413                _ => self.destructive_bs = false,
1414            },
1415            b'L' => {
1416                let (a, b) = (self.point(r)?, self.point(r)?);
1417                self.line(a, b);
1418                self.pen_at = b;
1419            }
1420            b'M' => {
1421                let m = self.val(r, 0)?;
1422                self.attrs.mode = if (1..=4).contains(&m) { m as u8 } else { 1 };
1423            }
1424            b'N' => self.noise(r)?,
1425            b'n' => {
1426                for _ in 0..6 {
1427                    self.val(r, 0)?;
1428                }
1429            }
1430            b'O' => {
1431                let (c, rad) = (self.point(r)?, self.val(r, 1)?);
1432                let ry = (rad as f64 * self.res.aspect()).round() as i32;
1433                let pts = self.ellipse_points(c, rad, ry, 0, 3600);
1434                self.shape(&pts);
1435            }
1436            b'P' => {
1437                let p = self.point(r)?;
1438                self.marker(p.0, p.1);
1439                self.pen_at = p;
1440            }
1441            b'Q' => {
1442                let (c, rx, ry) = (self.point(r)?, self.val(r, 1)?, self.val(r, 2)?);
1443                let pts = self.ellipse_points(c, rx, ry, 0, 3600);
1444                self.shape(&pts);
1445            }
1446            b'q' => {
1447                let n = self.val(r, 0)?;
1448                if (1..=180).contains(&n) && !self.in_loop() {
1449                    self.hold = Some(Instant::now() + Duration::from_millis(n as u64 * 1000 / 60));
1450                }
1451            }
1452            b'R' => {
1453                let (res, pal) = (self.val(r, 0)?, self.val(r, 0)?);
1454                let want = match res {
1455                    0 => Res::Low,
1456                    1 => Res::Medium,
1457                    _ => self.res,
1458                };
1459                if self.res != Res::High && want != self.res {
1460                    self.set_res(want);
1461                }
1462                match pal {
1463                    1 | 3 => self.desktop_palette(),
1464                    2 => self.regs = IG_PALETTE,
1465                    _ => {}
1466                }
1467            }
1468            b'S' => {
1469                let pen = self.val(r, 0)?;
1470                let (cr, cg, cb) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1471                let pen = if (0..=15).contains(&pen) { pen as u8 } else { 0 };
1472                let c = |v: i32| v.clamp(0, 7) as u16;
1473                let reg = self.reg(pen) as usize;
1474                self.regs[reg] = c(cr) << 8 | c(cg) << 4 | c(cb);
1475            }
1476            b's' => match self.val(r, 0)? {
1477                0 => self.vt52_escape(b'E'),
1478                1 => self.vt52_escape(b'd'),
1479                2 => self.vt52_escape(b'J'),
1480                3 => self.clear_screen(),
1481                4 => {
1482                    self.clear_screen();
1483                    (self.col, self.row) = (0, 0);
1484                }
1485                _ => {
1486                    self.reverse = false;
1487                    self.bg = 0;
1488                    self.fg = 3 & self.mask();
1489                    self.vt52_escape(b'E');
1490                }
1491            },
1492            b'T' => {
1493                let which = self.val(r, 0)?;
1494                if !(1..=2).contains(&which) {
1495                    return Ok(());
1496                }
1497                let kind = self.val(r, 0)?;
1498                let size = self.val(r, 0)?;
1499                if which == 1 {
1500                    self.attrs.marker.1 = if (1..=6).contains(&kind) { kind as u8 } else { 1 };
1501                    self.attrs.marker.2 = if (1..=8).contains(&size) { size } else { 1 };
1502                } else {
1503                    let kind = if (1..=7).contains(&kind) { kind as u8 } else { 1 };
1504                    if (1..42).contains(&size) {
1505                        self.attrs.line.style = kind;
1506                        self.attrs.line.width = if kind > 1 { 1 } else { size };
1507                    }
1508                    let ends = match size {
1509                        0 => Some((0, 0)),
1510                        50 => Some((1, 1)),
1511                        51 => Some((1, 0)),
1512                        52 => Some((0, 1)),
1513                        53 => Some((1, 2)),
1514                        54 => Some((2, 1)),
1515                        60 => Some((2, 2)),
1516                        61 => Some((2, 0)),
1517                        62 => Some((0, 2)),
1518                        63 => Some((2, 1)),
1519                        64 => Some((1, 2)),
1520                        _ => None,
1521                    };
1522                    if let Some(e) = ends {
1523                        self.attrs.line.ends = e;
1524                    }
1525                }
1526            }
1527            b't' => {
1528                let n = self.val(r, 0)?;
1529                if n > 0 && !self.in_loop() {
1530                    self.hold = Some(Instant::now() + Duration::from_secs(n.min(30) as u64));
1531                }
1532            }
1533            b'U' => {
1534                let (a, b, filled) = (self.point(r)?, self.point(r)?, self.val(r, 0)?);
1535                self.rounded_box((a.0, a.1, b.0, b.1), filled != 0);
1536            }
1537            b'W' => {
1538                let p = self.point(r)?;
1539                let reads = self.looping.as_ref().is_none_or(|lp| lp.kind == b'@');
1540                if reads {
1541                    self.last_text = r.text()?;
1542                }
1543                let text = self.last_text.clone();
1544                self.write_text(p.0, p.1, &text);
1545            }
1546            b'X' => self.extended(r)?,
1547            b'Z' => {
1548                let (a, b) = (self.point(r)?, self.point(r)?);
1549                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));
1550                for y in y0..=y1 {
1551                    self.fill_span(x0, x1, y);
1552                }
1553            }
1554            b'b' => match self.val(r, 0)? {
1555                20 => {
1556                    for _ in 0..6 {
1557                        self.val(r, 0)?;
1558                    }
1559                }
1560                22 | 23 => {
1561                    self.val(r, 0)?;
1562                }
1563                _ => {}
1564            },
1565            b'<' => {
1566                let (cr, kind, show) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1567                if (2..=10).contains(&kind) {
1568                    self.awaiting_zone = Some((cr != 0, show < 2));
1569                }
1570            }
1571            b'?' => {
1572                let q = self.val(r, 0)?;
1573                match q {
1574                    0 => self.replies.extend_from_slice(VERSION),
1575                    1 => {
1576                        self.val(r, 0)?;
1577                        let (c, rw) = (self.col as u8 + 32, self.row as u8 + 32);
1578                        self.replies.extend_from_slice(&[c, rw, 32]);
1579                    }
1580                    2 => {
1581                        self.val(r, 0)?;
1582                        self.replies.extend_from_slice(b"0,0,0:");
1583                    }
1584                    3 => self.replies.extend_from_slice(&[b'0' + self.res.number(), b':']),
1585                    _ => {}
1586                }
1587            }
1588            // The modified VT-52 commands.
1589            b'c' => {
1590                let (which, reg) = (self.val(r, 0)?, self.val(r, 0)?);
1591                let reg = reg as u8 & self.mask();
1592                if which == 0 {
1593                    self.bg = reg;
1594                } else {
1595                    self.fg = reg;
1596                }
1597            }
1598            b'd' => {
1599                for _ in 0..self.val(r, 0)?.clamp(0, 50) {
1600                    self.vt52_escape(b'M');
1601                }
1602            }
1603            b'i' => {
1604                let (kind, n) = (self.val(r, 0)?, self.val(r, 0)?);
1605                for _ in 0..n.clamp(0, 50) {
1606                    if kind == 0 {
1607                        self.vt52_escape(b'I');
1608                    } else {
1609                        self.vt52_escape(b'L');
1610                    }
1611                }
1612            }
1613            b'l' => match self.val(r, 0)? {
1614                0 => {
1615                    self.vt52_escape(b'l');
1616                    self.col = 0;
1617                }
1618                1 => self.vt52_escape(b'o'),
1619                _ => self.vt52_escape(b'K'),
1620            },
1621            b'm' => {
1622                let (dir, n) = (self.val(r, 0)?, self.val(r, 0)?);
1623                let esc = match dir {
1624                    1 => b'A',
1625                    2 => b'B',
1626                    3 => b'C',
1627                    4 => b'D',
1628                    _ => b'H',
1629                };
1630                for _ in 0..n.clamp(1, 80) {
1631                    self.vt52_escape(esc);
1632                }
1633            }
1634            b'p' => {
1635                let (c, rw) = (self.val(r, 0)?, self.val(r, 0)?);
1636                let (cols, rows) = self.text_size();
1637                self.col = (c.max(0) as usize).min(cols - 1);
1638                self.row = (rw.max(0) as usize).min(rows - 1);
1639            }
1640            b'r' => {
1641                if self.val(r, 0)? == 0 {
1642                    self.saved = (self.col, self.row);
1643                } else {
1644                    (self.col, self.row) = self.saved;
1645                }
1646            }
1647            b'v' => self.reverse = self.val(r, 0)? != 0,
1648            b'w' => self.wrap = self.val(r, 0)? != 0,
1649            _ => log::debug!("unknown IGS command {:?}", key as char),
1650        }
1651        Ok(())
1652    }
1653
1654    fn grab(&mut self, r: &mut Reader) -> Res2<()> {
1655        let (kind, op) = (self.val(r, 0)?, self.val(r, 0)?);
1656        let op = if (0..=15).contains(&op) { op as u8 } else { 3 };
1657        let a = self.point(r)?;
1658        match kind {
1659            0 => {
1660                let b = self.point(r)?;
1661                let d = self.point(r)?;
1662                self.blit(false, (a.0, a.1, b.0, b.1), false, d, op);
1663            }
1664            1 => {
1665                let b = self.point(r)?;
1666                self.memory_rect = (a.0, a.1, b.0, b.1);
1667                self.blit(false, self.memory_rect, true, (a.0.min(b.0), a.1.min(b.1)), op);
1668            }
1669            2 => self.blit(true, self.memory_rect, false, a, op),
1670            _ => {
1671                let b = self.point(r)?;
1672                let d = self.point(r)?;
1673                self.blit(true, (a.0, a.1, b.0, b.1), kind == 4, d, op);
1674            }
1675        }
1676        Ok(())
1677    }
1678
1679    /// `N`: MIDI and sound chip data, read past.
1680    fn noise(&mut self, r: &mut Reader) -> Res2<()> {
1681        match self.val(r, 0)? {
1682            0 | 1 | 3 | 4 => {
1683                let n = self.val(r, 0)?.clamp(0, 10_001) as usize;
1684                for _ in 0..n {
1685                    r.next()?;
1686                }
1687            }
1688            6 => {
1689                self.val(r, 0)?;
1690                self.val(r, 0)?;
1691            }
1692            _ => {}
1693        }
1694        Ok(())
1695    }
1696
1697    fn extended(&mut self, r: &mut Reader) -> Res2<()> {
1698        match self.val(r, 0)? {
1699            // Spray paint: markers at random in a rectangle.
1700            0 => {
1701                let (a, rx, ry, n) = (self.point(r)?, self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1702                for _ in 0..n.clamp(0, 9999) {
1703                    let x = a.0 + self.random((0, rx.clamp(0, 255)));
1704                    let y = a.1 + self.random((0, ry.clamp(0, 255)));
1705                    self.marker(x, y);
1706                }
1707            }
1708            // A color register, as the ST stores it (0x0RGB, in decimal).
1709            1 => {
1710                let (reg, v) = (self.val(r, 0)?, self.val(r, 0)?);
1711                self.regs[(reg & 15) as usize] = (v as u16) & 0x777;
1712            }
1713            2 => {
1714                let (a, b) = (self.val(r, 0)?, self.val(r, 0)?);
1715                if a == b {
1716                    let c = self.val(r, 0)?;
1717                    self.rand_big = (b, c);
1718                } else {
1719                    self.rand = (a, b);
1720                }
1721            }
1722            // The right mouse button's text: read past.
1723            3 => match self.val(r, 0)? {
1724                1 => {
1725                    self.val(r, 0)?;
1726                }
1727                2 => {
1728                    let (_on, _cr, len) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1729                    for _ in 0..len.clamp(0, 80) + 1 {
1730                        r.next()?;
1731                    }
1732                }
1733                _ => {}
1734            },
1735            // A mouse zone, and what it sends.
1736            4 => {
1737                let id = self.val(r, 0)?;
1738                if id >= 9997 {
1739                    if id == 9999 {
1740                        self.zones = vec![None; 48];
1741                    }
1742                    return Ok(());
1743                }
1744                let (a, b, len) = (self.point(r)?, self.point(r)?, self.val(r, 0)?);
1745                let mut text = Vec::new();
1746                for _ in 0..len.clamp(0, 80) {
1747                    text.push(r.next()?);
1748                }
1749                r.next()?;
1750                if let Some(z) = self.zones.get_mut(id.clamp(0, 47) as usize) {
1751                    *z = Some((a.0.min(b.0), a.1.min(b.1), a.0.max(b.0), a.1.max(b.1), text));
1752                }
1753            }
1754            5 => {
1755                if matches!(self.val(r, 0)?, 2 | 3) {
1756                    self.val(r, 0)?;
1757                    self.val(r, 0)?;
1758                }
1759            }
1760            6 => {
1761                self.val(r, 0)?;
1762            }
1763            // A user fill pattern: 16 rows of 16 characters, X for a bit.
1764            7 => {
1765                let slot = self.val(r, 0)?.clamp(0, 7) as usize;
1766                let mut rows = [0u16; 16];
1767                for row in &mut rows {
1768                    let text = r.text()?;
1769                    *row =
1770                        text.iter().take(16).enumerate().fold(0, |acc, (i, &c)| {
1771                            if c.eq_ignore_ascii_case(&b'X') { acc | 0x8000 >> i } else { acc }
1772                        });
1773                }
1774                self.user_patterns[slot] = rows;
1775            }
1776            // Rotating color registers: done at once (no animation yet).
1777            8 => {
1778                let (a, b, times, _delay) = (self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?, self.val(r, 0)?);
1779                let (lo, hi) = (a.min(b).clamp(0, 15) as usize, a.max(b).clamp(0, 15) as usize);
1780                if times > 0 && lo < hi {
1781                    let n = times as usize % (hi - lo + 1);
1782                    if a < b {
1783                        self.regs[lo..=hi].rotate_right(n);
1784                    } else {
1785                        self.regs[lo..=hi].rotate_left(n);
1786                    }
1787                }
1788            }
1789            // Store IG commands to run later.
1790            9 => match self.val(r, 0)? {
1791                0 => {
1792                    let mut stored = Vec::new();
1793                    while !stored.ends_with(b"||}") {
1794                        stored.push(r.next()?);
1795                    }
1796                    stored.truncate(stored.len() - 3);
1797                    self.stored = stored;
1798                }
1799                1 => self.run_stored = Some(self.stored.clone()),
1800                _ => self.stored.clear(),
1801            },
1802            10 => self.pen_at = self.point(r)?,
1803            _ => {}
1804        }
1805        Ok(())
1806    }
1807
1808    /// A click at a point of the bitmap: what to send the BBS, if a zone
1809    /// was being waited for.
1810    pub fn click(&mut self, x: i32, y: i32) -> Option<Vec<u8>> {
1811        let (cr, send) = self.awaiting_zone?;
1812        let hit = self.zones[..47]
1813            .iter()
1814            .flatten()
1815            .find(|z| x >= z.0 && x <= z.2 && y >= z.1 && y <= z.3)
1816            .or(self.zones[47].as_ref())?;
1817        let mut out = if send { hit.4.clone() } else { Vec::new() };
1818        self.awaiting_zone = None;
1819        if cr && send {
1820            out.push(b'\r');
1821        }
1822        Some(out)
1823    }
1824}
1825
1826#[cfg(test)]
1827mod tests {
1828    use super::*;
1829
1830    fn igs(data: &[u8]) -> Igs {
1831        let mut g = Igs::default();
1832        g.feed(data);
1833        g
1834    }
1835
1836    fn at(g: &Igs, x: usize, y: usize) -> u8 {
1837        g.pixels[y * g.width() + x]
1838    }
1839
1840    #[test]
1841    fn numbers_as_ig_reads_them() {
1842        let mut r = Reader { data: b"12,7:_\r\n34,1\r\n5\r\n2,x,+10,-3,!99,r,12345,", pos: 0 };
1843        let toks: Vec<Tok> = (0..9).map(|_| r.token().unwrap()).collect();
1844        assert_eq!(
1845            toks,
1846            [
1847                Tok::Num(12),
1848                Tok::Num(7),
1849                Tok::Num(34),
1850                Tok::Num(152),
1851                Tok::X,
1852                Tok::Plus(10),
1853                Tok::Minus(3),
1854                Tok::Bang(99),
1855                Tok::Rand,
1856            ]
1857        );
1858        assert_eq!(r.token().unwrap(), Tok::Num(0), "five digits are too many");
1859        assert!(Reader { data: b"12", pos: 0 }.token().is_err());
1860    }
1861
1862    #[test]
1863    fn lines_boxes_and_chains() {
1864        // A solid black box (pen 1 is register 3 in medium resolution),
1865        // then a line in red (pen 2, register 1), chained; then text.
1866        let g = igs(b"G#A>1,1,0:B>10,10,20,20,0:C>1,2:L 0,50,100,50:ok");
1867        assert_eq!(at(&g, 15, 15), 3);
1868        assert_eq!(at(&g, 25, 15), 0);
1869        assert_eq!(at(&g, 50, 50), 1);
1870        // The text after the chain is text: "ok" at the cursor, in black.
1871        assert!((0..8).any(|y| (0..16).any(|x| at(&g, x, y) == 3)));
1872    }
1873
1874    #[test]
1875    fn commands_split_across_reads() {
1876        let mut g = Igs::default();
1877        g.feed(b"G#C>2,2:B>0,0,");
1878        assert_eq!(at(&g, 5, 5), 0);
1879        g.feed(b"10,10,0:");
1880        assert_eq!(at(&g, 5, 5), 1);
1881    }
1882
1883    #[test]
1884    fn loops_step_and_read_data() {
1885        // Vertical lines at x = 0, 10, 20, 30 from a loop.
1886        let g = igs(b"G#C>1,1:&>0,30,10,0,L,4,x,0,x,9:");
1887        for x in [0, 10, 20, 30] {
1888            assert_eq!(at(&g, x, 5), 3, "x={x}");
1889        }
1890        assert_eq!(at(&g, 5, 5), 0);
1891        assert_eq!(at(&g, 40, 5), 0);
1892        // READ DATA: the same command with constant parameters, twice.
1893        let g = igs(b"G#C>1,1:&>0,0,1,0,L,8,100,0,100,9,200,0,200,9:");
1894        assert_eq!((at(&g, 100, 5), at(&g, 200, 5)), (3, 3));
1895    }
1896
1897    #[test]
1898    fn flood_fill_and_patterns() {
1899        // An outlined box, then flood its inside with pattern 4 (50%).
1900        let g = igs(b"G#A>0,1,1:B>0,0,40,20,0:A>2,4,0:F>10,10:");
1901        assert_eq!(at(&g, 0, 0), 3, "the border");
1902        let inside: Vec<u8> = (8..12).map(|x| at(&g, x, 10)).collect();
1903        assert!(inside.contains(&3) && inside.contains(&0), "{inside:?}");
1904        assert_eq!(at(&g, 50, 10), 0, "outside");
1905    }
1906
1907    #[test]
1908    fn vt52_text_and_queries() {
1909        let mut g = igs(b"\x1bY\x22\x25X\x1bb\x01Y");
1910        // "X" at column 5, row 2, black; "Y" after it in red.
1911        assert!((16..24).any(|y| (40..48).any(|x| at(&g, x, y) == 3)));
1912        assert!((16..24).any(|y| (48..56).any(|x| at(&g, x, y) == 1)));
1913        g.feed(b"G#?>0:?>3:");
1914        assert_eq!(g.replies, b"2.191:");
1915        // Low resolution: sixteen colors.
1916        g.feed(b"G#R>0,0:C>2,15:Z>0,0,9,9:");
1917        assert_eq!((g.width(), at(&g, 5, 5)), (320, 13));
1918    }
1919
1920    #[test]
1921    fn mouse_zones() {
1922        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:");
1923        assert_eq!(g.click(50, 50), Some(b"f/L\r".to_vec()));
1924        assert_eq!(g.click(50, 50), None, "only while waiting");
1925        g.feed(b"G#< 0,2,1:");
1926        assert_eq!(g.click(300, 150), Some(b"Q".to_vec()));
1927    }
1928
1929    #[test]
1930    fn pauses_hold_the_stream() {
1931        let mut g = igs(b"G#t>1:C>2,2:Z>0,0,9,9:");
1932        assert!(g.hold.is_some());
1933        assert_eq!(at(&g, 5, 5), 0);
1934        g.poll(Instant::now() + Duration::from_secs(2));
1935        assert_eq!(at(&g, 5, 5), 1);
1936    }
1937}