Skip to main content

badterm_core/
petscii.rs

1//! Commodore 64 BBSes: PETSCII, on a 40x25 screen in the C64's colors;
2//! and the Commodore 128's 80-column screen (its VDC chip): the same
3//! PETSCII on 80x25, in the VDC's RGBI colors.
4//!
5//! Printable bytes become screen codes (the order the C64's characters are
6//! stored in, which BadTerm's C64 fonts use; reverse video is the second
7//! half of the font), and control bytes move the cursor, change the color,
8//! switch between the upper case/graphics and lower/upper case sets, and so
9//! on, as the C64's screen editor does.
10
11use crate::ansi::TermEvent;
12use crate::palette::Rgb;
13use crate::screen::{Color, Screen};
14
15pub const COLS: usize = 40;
16pub const ROWS: usize = 25;
17/// The CTerm font IDs of the two character sets (as SyncTERM numbers them).
18pub const FONT_UPPER: u8 = 32;
19pub const FONT_LOWER: u8 = 33;
20/// The C128's, for its 80-column screen.
21pub const C128_COLS: usize = 80;
22pub const FONT_C128_UPPER: u8 = 34;
23pub const FONT_C128_LOWER: u8 = 35;
24
25/// The C64's 16 colors (the "Pepto" measurements).
26pub const PALETTE: [Rgb; 16] = [
27    0x000000, 0xFFFFFF, 0x68372B, 0x70A4B2, 0x6F3D86, 0x588D43, 0x352879, 0xB8C76F, 0x6F4F25, 0x433900, 0x9A6759,
28    0x444444, 0x6C6C6C, 0x9AD284, 0x6C5EB5, 0x959595,
29];
30/// The C128's 80-column colors, in the C64's order (RGBI, as on a CGA
31/// monitor): black, white, dark red, light cyan, light purple, dark
32/// green, dark blue, light yellow, dark purple, brown, light red, dark
33/// cyan, dark gray, light green, light blue, light gray.
34pub const VDC_PALETTE: [Rgb; 16] = [
35    0x000000, 0xFFFFFF, 0xAA0000, 0x55FFFF, 0xFF55FF, 0x00AA00, 0x0000AA, 0xFFFF55, 0xAA00AA, 0xAA5500, 0xFF5555,
36    0x00AAAA, 0x555555, 0x55FF55, 0x5555FF, 0xAAAAAA,
37];
38/// Light blue text on black, as C64 terminal programs start.
39const DEFAULT_FG: usize = 14;
40
41pub fn color(i: usize) -> Color {
42    Color::Rgb(PALETTE[i & 15])
43}
44
45/// The C64 screen editor's state.
46#[derive(Debug, Default)]
47pub struct Petscii {
48    reverse: bool,
49    /// The C128's 80-column screen, not the C64's.
50    c128: bool,
51}
52
53impl Petscii {
54    pub fn c128() -> Petscii {
55        Petscii { reverse: false, c128: true }
56    }
57
58    fn color(&self, i: usize) -> Color {
59        Color::Rgb(if self.c128 { VDC_PALETTE[i & 15] } else { PALETTE[i & 15] })
60    }
61
62    fn fonts(&self) -> (u8, u8) {
63        if self.c128 { (FONT_C128_UPPER, FONT_C128_LOWER) } else { (FONT_UPPER, FONT_LOWER) }
64    }
65
66    /// A fresh C64 screen: cleared, colors and character set as at power-on.
67    pub fn start(&mut self, s: &mut Screen) {
68        self.reverse = false;
69        // The C128 starts light cyan in 80 columns.
70        s.pen.fg = self.color(if self.c128 { 3 } else { DEFAULT_FG });
71        s.pen.bg = self.color(0);
72        let (upper, _) = self.fonts();
73        s.look.fonts[0] = upper;
74        s.base_font = upper;
75        s.autowrap = true;
76        s.erase_display(2);
77        s.move_to(0, 0);
78    }
79
80    pub fn byte(&mut self, s: &mut Screen, b: u8, events: &mut Vec<TermEvent>) {
81        let pal = if self.c128 { VDC_PALETTE } else { PALETTE };
82        let ink = |s: &mut Screen, i: usize| s.pen.fg = Color::Rgb(pal[i & 15]);
83        let (upper, lower) = self.fonts();
84        match b {
85            0x05 => ink(s, 1),
86            0x07 => events.push(TermEvent::Bell),
87            0x0D | 0x8D => {
88                s.carriage_return();
89                s.line_feed();
90                self.reverse = false;
91            }
92            0x0E => s.look.fonts[0] = lower,
93            0x8E => s.look.fonts[0] = upper,
94            0x11 => s.move_by(0, 1),
95            0x91 => s.move_by(0, -1),
96            0x1D => right(s),
97            0x9D => left(s),
98            0x12 => self.reverse = true,
99            0x92 => self.reverse = false,
100            0x13 => s.move_to(0, 0),
101            0x93 => {
102                s.erase_display(2);
103                s.move_to(0, 0);
104            }
105            // DEL: back one, closing up the line.
106            0x14 => {
107                if s.cursor().0 > 0 {
108                    s.move_by(-1, 0);
109                    s.delete_chars(1);
110                }
111            }
112            0x94 => s.insert_chars(1),
113            0x1C => ink(s, 2),
114            0x1E => ink(s, 5),
115            0x1F => ink(s, 6),
116            0x81 => ink(s, 8),
117            0x90 => ink(s, 0),
118            0x95 => ink(s, 9),
119            0x96 => ink(s, 10),
120            0x97 => ink(s, 11),
121            0x98 => ink(s, 12),
122            0x99 => ink(s, 13),
123            0x9A => ink(s, 14),
124            0x9B => ink(s, 15),
125            0x9C => ink(s, 4),
126            0x9E => ink(s, 7),
127            0x9F => ink(s, 3),
128            _ => {
129                if let Some(code) = screen_code(b) {
130                    s.put(if self.reverse { code | 0x80 } else { code });
131                }
132            }
133        }
134    }
135}
136
137/// Cursor right, on to the next line at the end of one.
138fn right(s: &mut Screen) {
139    let (x, y) = s.cursor();
140    if x + 1 < s.cols() {
141        s.move_by(1, 0);
142    } else if y + 1 < s.rows() {
143        s.move_to(0, y + 1);
144    } else {
145        s.carriage_return();
146        s.line_feed();
147    }
148}
149
150/// Cursor left, back to the end of the line above at the start of one.
151fn left(s: &mut Screen) {
152    let (x, y) = s.cursor();
153    if x > 0 {
154        s.move_by(-1, 0);
155    } else if y > 0 {
156        s.move_to(s.cols() - 1, y - 1);
157    }
158}
159
160/// The screen code of a printable PETSCII byte; None for the controls.
161pub fn screen_code(b: u8) -> Option<u8> {
162    Some(match b {
163        0x20..=0x3F => b,
164        0x40..=0x5F => b - 0x40,
165        0x60..=0x7F => b - 0x20,
166        0xA0..=0xBF => b - 0x40,
167        0xC0..=0xFE => b - 0x80,
168        0xFF => 0x5E,
169        _ => return None,
170    })
171}
172
173/// What a typed character sends to a C64 BBS. In the C64's default case,
174/// unshifted letters show as capitals, so lower-case letters send those
175/// codes and capitals the shifted ones.
176pub fn encode_char(c: char) -> Option<u8> {
177    match c {
178        'a'..='z' => Some(c as u8 - b'a' + 0x41),
179        'A'..='Z' => Some(c as u8 - b'A' + 0xC1),
180        ' '..='@' | '[' | ']' => Some(c as u8),
181        '\u{a3}' => Some(0x5C),
182        '\u{2191}' | '^' => Some(0x5E),
183        '\u{2190}' | '_' => Some(0x5F),
184        _ => None,
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::screen::Screen;
192
193    fn run(bytes: &[u8]) -> (Screen, Petscii) {
194        let mut s = Screen::new(COLS, ROWS, 0);
195        let mut p = Petscii::default();
196        p.start(&mut s);
197        let mut ev = Vec::new();
198        for &b in bytes {
199            p.byte(&mut s, b, &mut ev);
200        }
201        (s, p)
202    }
203
204    #[test]
205    fn text_colors_and_reverse() {
206        // "HI" in white, then reversed red "A", then a newline.
207        let (s, _) = run(b"\x05HI\x1c\x12A\x92\x0dX");
208        let row = s.row(0);
209        assert_eq!((row[0].ch, row[1].ch), (0x08, 0x09));
210        assert_eq!(row[0].fg, color(1));
211        assert_eq!((row[2].ch, row[2].fg), (0x01 | 0x80, color(2)));
212        assert_eq!(s.row(1)[0].ch, 0x18);
213        assert_eq!(s.cursor(), (1, 1));
214    }
215
216    #[test]
217    fn cursor_clear_and_charsets() {
218        let (s, _) = run(b"ABC\x9d\x9d\x14\x13\x11\x1d*\x0e");
219        // On B, DEL takes the A before it and closes up the line.
220        assert_eq!((s.row(0)[0].ch, s.row(0)[1].ch), (0x02, 0x03));
221        assert_eq!(s.row(1)[1].ch, b'*');
222        assert_eq!(s.look.fonts[0], FONT_LOWER);
223        let (s, _) = run(b"ABC\x93");
224        assert!(s.row(0).iter().all(|c| c.ch == b' '));
225        assert_eq!(s.cursor(), (0, 0));
226        // Graphics: shifted bytes and their copies land on the same glyph.
227        assert_eq!(screen_code(0xC1), screen_code(0x61));
228        assert_eq!(screen_code(0xA0), Some(0x60));
229        assert_eq!((encode_char('a'), encode_char('A'), encode_char('1')), (Some(0x41), Some(0xC1), Some(b'1')));
230    }
231}