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