Skip to main content

game_toolkit_dosvideo/
input.rs

1//! Keyboard and mouse, the way a DOS program sees them.
2//!
3//! The keyboard comes in both flavors DOS games used: a table of which keys are held, indexed
4//! by XT (set 1) scancode, like a custom `INT 9` handler builds it, and the BIOS keyboard buffer
5//! of `(scancode, ASCII)` pairs for `kbhit()` / `getch()` style input. Port `0x60` returns the
6//! last make/break code.
7//!
8//! The mouse is an `INT 33h` driver: positions are in the driver's *virtual screen*
9//! coordinates, which depend on the BIOS video mode, not on what the CRTC really shows. In mode
10//! 13h that is 640x200, so X has to be halved, and a Mode X program at 320x240 still gets Y in
11//! `0..200` until it sets the range itself, both well-known DOS annoyances.
12
13use std::collections::VecDeque;
14
15/// XT scancodes (set 1 make codes).
16pub mod sc {
17    pub const ESC: u8 = 0x01;
18    pub const N1: u8 = 0x02;
19    pub const N2: u8 = 0x03;
20    pub const N3: u8 = 0x04;
21    pub const N4: u8 = 0x05;
22    pub const N5: u8 = 0x06;
23    pub const N6: u8 = 0x07;
24    pub const N7: u8 = 0x08;
25    pub const N8: u8 = 0x09;
26    pub const N9: u8 = 0x0A;
27    pub const N0: u8 = 0x0B;
28    pub const MINUS: u8 = 0x0C;
29    pub const EQUALS: u8 = 0x0D;
30    pub const BACKSPACE: u8 = 0x0E;
31    pub const TAB: u8 = 0x0F;
32    pub const Q: u8 = 0x10;
33    pub const W: u8 = 0x11;
34    pub const E: u8 = 0x12;
35    pub const R: u8 = 0x13;
36    pub const T: u8 = 0x14;
37    pub const Y: u8 = 0x15;
38    pub const U: u8 = 0x16;
39    pub const I: u8 = 0x17;
40    pub const O: u8 = 0x18;
41    pub const P: u8 = 0x19;
42    pub const LBRACKET: u8 = 0x1A;
43    pub const RBRACKET: u8 = 0x1B;
44    pub const ENTER: u8 = 0x1C;
45    pub const CTRL: u8 = 0x1D;
46    pub const A: u8 = 0x1E;
47    pub const S: u8 = 0x1F;
48    pub const D: u8 = 0x20;
49    pub const F: u8 = 0x21;
50    pub const G: u8 = 0x22;
51    pub const H: u8 = 0x23;
52    pub const J: u8 = 0x24;
53    pub const K: u8 = 0x25;
54    pub const L: u8 = 0x26;
55    pub const SEMICOLON: u8 = 0x27;
56    pub const APOSTROPHE: u8 = 0x28;
57    pub const BACKTICK: u8 = 0x29;
58    pub const LSHIFT: u8 = 0x2A;
59    pub const BACKSLASH: u8 = 0x2B;
60    pub const Z: u8 = 0x2C;
61    pub const X: u8 = 0x2D;
62    pub const C: u8 = 0x2E;
63    pub const V: u8 = 0x2F;
64    pub const B: u8 = 0x30;
65    pub const N: u8 = 0x31;
66    pub const M: u8 = 0x32;
67    pub const COMMA: u8 = 0x33;
68    pub const PERIOD: u8 = 0x34;
69    pub const SLASH: u8 = 0x35;
70    pub const RSHIFT: u8 = 0x36;
71    pub const KP_STAR: u8 = 0x37;
72    pub const ALT: u8 = 0x38;
73    pub const SPACE: u8 = 0x39;
74    pub const CAPSLOCK: u8 = 0x3A;
75    pub const F1: u8 = 0x3B;
76    pub const F2: u8 = 0x3C;
77    pub const F3: u8 = 0x3D;
78    pub const F4: u8 = 0x3E;
79    pub const F5: u8 = 0x3F;
80    pub const F6: u8 = 0x40;
81    pub const F7: u8 = 0x41;
82    pub const F8: u8 = 0x42;
83    pub const F9: u8 = 0x43;
84    pub const F10: u8 = 0x44;
85    pub const NUMLOCK: u8 = 0x45;
86    pub const SCROLLLOCK: u8 = 0x46;
87    pub const HOME: u8 = 0x47;
88    pub const UP: u8 = 0x48;
89    pub const PGUP: u8 = 0x49;
90    pub const KP_MINUS: u8 = 0x4A;
91    pub const LEFT: u8 = 0x4B;
92    pub const KP5: u8 = 0x4C;
93    pub const RIGHT: u8 = 0x4D;
94    pub const KP_PLUS: u8 = 0x4E;
95    pub const END: u8 = 0x4F;
96    pub const DOWN: u8 = 0x50;
97    pub const PGDN: u8 = 0x51;
98    pub const INSERT: u8 = 0x52;
99    pub const DELETE: u8 = 0x53;
100    pub const F11: u8 = 0x57;
101    pub const F12: u8 = 0x58;
102}
103
104/// One entry of the BIOS keyboard buffer.
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub struct KeyPress {
107    /// XT scancode.
108    pub scan: u8,
109    /// ASCII (CP437) code, `0` for extended keys (arrows, F-keys, ...).
110    pub ascii: u8,
111}
112
113/// Keyboard state.
114#[derive(Clone, Debug)]
115pub struct Keyboard {
116    down: [bool; 128],
117    buffer: VecDeque<KeyPress>,
118    last_code: u8,
119}
120
121impl Default for Keyboard {
122    fn default() -> Self {
123        Self {
124            down: [false; 128],
125            buffer: VecDeque::new(),
126            last_code: 0,
127        }
128    }
129}
130
131/// The BIOS buffer holds 15 keys; further presses beep and are dropped.
132const BIOS_BUFFER: usize = 15;
133
134impl Keyboard {
135    /// Is the key with this scancode held?
136    pub fn down(&self, scan: u8) -> bool {
137        self.down[(scan & 0x7F) as usize]
138    }
139
140    /// The whole held-key table, like the array a custom keyboard ISR fills.
141    pub fn table(&self) -> &[bool; 128] {
142        &self.down
143    }
144
145    /// Is there a key in the BIOS buffer? (`INT 16h, AH=01h`, `kbhit()`.)
146    pub fn hit(&self) -> bool {
147        !self.buffer.is_empty()
148    }
149
150    /// Take the next key from the BIOS buffer (`INT 16h, AH=00h`, `getch()`), without waiting.
151    pub fn read(&mut self) -> Option<KeyPress> {
152        self.buffer.pop_front()
153    }
154
155    /// Drop all buffered keys (what games did before "press any key").
156    pub fn flush(&mut self) {
157        self.buffer.clear();
158    }
159
160    /// Last code on port 0x60: the scancode, with bit 7 set for a release.
161    pub fn port60(&self) -> u8 {
162        self.last_code
163    }
164
165    /// Feed a key press (host side). `ascii` is 0 for keys without a character.
166    pub fn press(&mut self, scan: u8, ascii: u8) {
167        let s = (scan & 0x7F) as usize;
168        self.down[s] = true;
169        self.last_code = scan & 0x7F;
170        if self.buffer.len() < BIOS_BUFFER {
171            self.buffer.push_back(KeyPress {
172                scan: scan & 0x7F,
173                ascii,
174            });
175        }
176    }
177
178    /// Feed a key release (host side).
179    pub fn release(&mut self, scan: u8) {
180        self.down[(scan & 0x7F) as usize] = false;
181        self.last_code = scan | 0x80;
182    }
183
184    pub fn shift(&self) -> bool {
185        self.down(sc::LSHIFT) || self.down(sc::RSHIFT)
186    }
187
188    /// The ASCII code of a scancode on a US keyboard.
189    pub fn ascii_for(scan: u8, shift: bool) -> u8 {
190        const LOWER: &[u8; 0x3A] =
191            b"\0\x1b1234567890-=\x08\tqwertyuiop[]\r\0asdfghjkl;'`\0\\zxcvbnm,./\0*\0 ";
192        const UPPER: &[u8; 0x3A] =
193            b"\0\x1b!@#$%^&*()_+\x08\tQWERTYUIOP{}\r\0ASDFGHJKL:\"~\0|ZXCVBNM<>?\0*\0 ";
194        let t = if shift { UPPER } else { LOWER };
195        t.get(scan as usize).copied().unwrap_or(0)
196    }
197}
198
199/// `INT 33h` mouse driver state.
200#[derive(Clone, Copy, Debug)]
201pub struct Mouse {
202    /// Position in driver coordinates.
203    pub x: i32,
204    pub y: i32,
205    /// Bit 0 left, bit 1 right, bit 2 middle.
206    pub buttons: u8,
207    pub(crate) range: [i32; 4],
208    pub(crate) custom_range: bool,
209    pub(crate) visible: bool,
210    presses: [u16; 3],
211}
212
213impl Default for Mouse {
214    fn default() -> Self {
215        Self {
216            x: 0,
217            y: 0,
218            buttons: 0,
219            range: [0, 639, 0, 199],
220            custom_range: false,
221            visible: false,
222            presses: [0; 3],
223        }
224    }
225}
226
227impl Mouse {
228    pub fn left(&self) -> bool {
229        self.buttons & 1 != 0
230    }
231
232    pub fn right(&self) -> bool {
233        self.buttons & 2 != 0
234    }
235
236    pub fn middle(&self) -> bool {
237        self.buttons & 4 != 0
238    }
239
240    /// Function 01h: show the mouse cursor. The driver draws nothing itself (draw the pointer
241    /// in your program, e.g. with `tui::mouse_cursor` in text mode); the host hides the OS
242    /// pointer over the display while the cursor is shown. A mode set hides it again.
243    pub fn show(&mut self) {
244        self.visible = true;
245    }
246
247    /// Function 02h: hide the mouse cursor (the host shows the OS pointer again).
248    pub fn hide(&mut self) {
249        self.visible = false;
250    }
251
252    /// Whether the program has shown the mouse cursor.
253    pub fn visible(&self) -> bool {
254        self.visible
255    }
256
257    /// Function 05h: how often a button was pressed since the last call.
258    pub fn take_presses(&mut self, button: usize) -> u16 {
259        std::mem::take(&mut self.presses[button.min(2)])
260    }
261
262    /// Function 07h / 08h: limit the driver coordinates.
263    pub fn set_range(&mut self, x_min: i32, x_max: i32, y_min: i32, y_max: i32) {
264        self.range = [x_min, x_max.max(x_min), y_min, y_max.max(y_min)];
265        self.custom_range = true;
266        self.x = self.x.clamp(self.range[0], self.range[1]);
267        self.y = self.y.clamp(self.range[2], self.range[3]);
268    }
269
270    /// Host side: put the pointer at a normalized position over the active display.
271    pub fn set_normalized(&mut self, fx: f32, fy: f32) {
272        let [x0, x1, y0, y1] = self.range;
273        let fx = fx.clamp(0.0, 0.999_99);
274        let fy = fy.clamp(0.0, 0.999_99);
275        self.x = x0 + (fx * (x1 - x0 + 1) as f32) as i32;
276        self.y = y0 + (fy * (y1 - y0 + 1) as f32) as i32;
277    }
278
279    /// Host side: update the buttons, counting presses.
280    pub fn set_buttons(&mut self, buttons: u8) {
281        for b in 0..3 {
282            if buttons & (1 << b) != 0 && self.buttons & (1 << b) == 0 {
283                self.presses[b] = self.presses[b].saturating_add(1);
284            }
285        }
286        self.buttons = buttons;
287    }
288
289    /// Default driver range for a BIOS mode (what the driver resets to on a mode set).
290    pub(crate) fn reset_for_mode(&mut self, mode: u8, hercules: bool) {
291        self.range = match mode {
292            0x0F | 0x10 => [0, 639, 0, 349],
293            0x11 | 0x12 => [0, 639, 0, 479],
294            0x07 if hercules => [0, 639, 0, 199],
295            0xFF => self.range,
296            _ => [0, 639, 0, 199],
297        };
298        self.custom_range = false;
299        self.visible = false;
300        self.x = (self.range[0] + self.range[1]) / 2;
301        self.y = (self.range[2] + self.range[3]) / 2;
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn ascii_table() {
311        assert_eq!(Keyboard::ascii_for(sc::A, false), b'a');
312        assert_eq!(Keyboard::ascii_for(sc::A, true), b'A');
313        assert_eq!(Keyboard::ascii_for(sc::N1, true), b'!');
314        assert_eq!(Keyboard::ascii_for(sc::SPACE, false), b' ');
315        assert_eq!(Keyboard::ascii_for(sc::ENTER, false), b'\r');
316        assert_eq!(Keyboard::ascii_for(sc::UP, false), 0);
317    }
318
319    #[test]
320    fn mode13h_mouse_reports_640_wide() {
321        let mut m = Mouse::default();
322        m.reset_for_mode(0x13, false);
323        m.set_normalized(0.5, 0.5);
324        assert_eq!((m.x, m.y), (320, 100));
325    }
326}