@8bitscript/atari8 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,147 @@
1
+ // @8bitscript/atari8/joystick — the control ports, one snapshot a frame.
2
+ //
3
+ // Named by "8bitscript".exports["./joystick"] in this package's package.json:
4
+ //
5
+ // import { joystick, Joystick } from "@8bitscript/atari8/joystick";
6
+ //
7
+ // joystick.scan();
8
+ // if (joystick.left(Joystick.PORT_1)) { ... }
9
+ //
10
+ // Hardware-level, Atari-only surface, the layer a portable input capability
11
+ // will sit on — the same shape as @8bitscript/c64/joystick, deliberately:
12
+ // `scan()` once a frame into a snapshot, then `up`/`down`/`left`/`right`/
13
+ // `fire` answering from it, and the same `Joystick.UP`/`DOWN`/`LEFT`/
14
+ // `RIGHT`/`FIRE` bit values. That is not a coincidence to be tidied away
15
+ // later: the Atari's own masks in the SDK's `atari.h` (`JOY_UP_MASK` $01,
16
+ // `JOY_DOWN_MASK` $02, `JOY_LEFT_MASK` $04, `JOY_RIGHT_MASK` $08,
17
+ // `JOY_BTN_1_MASK` $10) are bit for bit the C64's, so the two machines'
18
+ // layers agree without either one pretending.
19
+ //
20
+ // ---- why this reads the OS shadows, not the hardware ----------------------
21
+ //
22
+ // A digital joystick is five switches to ground. The hardware is PIA's
23
+ // PORTA ($D300) — four direction bits per port, 0 where a switch is
24
+ // closed — and GTIA's TRIG0-3 ($D010-$D013), 0 while the button is down.
25
+ // A program could read those directly, and one that has cleared NMIEN's
26
+ // VBI bit and taken the machine over must. This layer does not, because on
27
+ // an 8BitScript build it does not have to: an Atari program built by this
28
+ // toolchain contains no `sei` and never disables the OS vertical blank
29
+ // (checked in the linked ELF; see ../AGENTS.md), so the OS's own VBI
30
+ // refreshes STICK0-3 ($0278-$027B) and STRIG0-3 ($0284-$0287) every single
31
+ // frame, already debounced and already split per port. Reading the shadow
32
+ // is one `lda abs,x` against PORTA's mask-and-shift, and it cannot race the
33
+ // OS the way the C64's matrix scan races the KERNAL.
34
+ //
35
+ // The one thing it costs: the shadows are only as fresh as the last VBI, so
36
+ // `scan()` belongs right after `waitFrame()`, which is where a frame's
37
+ // input reads belong anyway.
38
+ //
39
+ // ---- a mouse in a port IS the joystick ------------------------------------
40
+ //
41
+ // An ST or Amiga mouse and a Trak-Ball are not separate devices to the
42
+ // machine: they sit on a joystick port and signal movement as quadrature on
43
+ // the very direction bits this file reads. So with `--hardware mouse=st` in
44
+ // the port a program is scanning, `bits()` returns mouse motion and `up()`
45
+ // and friends go true while the mouse moves — that is the hardware being
46
+ // honest, not this layer being wrong. Seen here: test/layers-probe.8bs paints
47
+ // its border green when every input reads idle, and under `mouse=st` it goes
48
+ // red while `mouse=paddles` and `mouse=koala` stay green, because paddles and
49
+ // a Koala Pad are on POKEY's POT lines instead and leave the stick bits
50
+ // alone. A program that wants both puts them in different ports
51
+ // (`--hardware mouseport=2`), or reads the pointer and does not scan that
52
+ // port as a stick.
53
+ //
54
+ // ---- how many ports there are is a fact, not a guess -----------------------
55
+ //
56
+ // The 400 and 800 have four ports; every XL and XE has two, and on those
57
+ // machines PORTB is not a joystick port at all but the memory-control
58
+ // register (see ./banks.8bs). So the port count is a property of the
59
+ // *model* the build was made for — `input.joysticks` on the fact sheet,
60
+ // which the catalog's `model` option sets — and `scan()` reads exactly that
61
+ // many. `#fact(input.joysticks)` is a compile-time constant, so on an
62
+ // 800XL build the loop is two iterations and ports 3 and 4 cost nothing;
63
+ // on an 800 build it is four. Asking for a port the machine does not have
64
+ // answers "nothing pushed" rather than reading PORTB and calling the
65
+ // memory-control bits a joystick.
66
+ import { attract } from "./index.8bs";
67
+
68
+ // The OS's per-port shadows, refreshed by its VBI: four directions at
69
+ // STICK0 and one button at STRIG0, one byte each, consecutive.
70
+ const STICK0: usmallint = 0x0278;
71
+ const STRIG0: usmallint = 0x0284;
72
+
73
+ // The last scan, one byte per port, `Joystick.*` bits, 1 = pushed — the
74
+ // hardware's active-low sense inverted once, here, so nothing above has to
75
+ // think about it. All zero until the first scan.
76
+ let state: array<utinyint, 4>;
77
+
78
+ export namespace Joystick {
79
+ const PORT_1: utinyint = 0;
80
+ const PORT_2: utinyint = 1;
81
+ // Only on a 400 or an 800; `Joystick.PORTS` says whether this build has
82
+ // them, and `scan()` leaves them at zero when it does not.
83
+ const PORT_3: utinyint = 2;
84
+ const PORT_4: utinyint = 3;
85
+
86
+ const UP: utinyint = 1;
87
+ const DOWN: utinyint = 2;
88
+ const LEFT: utinyint = 4;
89
+ const RIGHT: utinyint = 8;
90
+ const FIRE: utinyint = 16;
91
+
92
+ // What this build's model has: 4 on a 400 or 800, 2 on every XL/XE.
93
+ // The same number as `Input.JOYSTICKS` from @8bitscript/system — the
94
+ // fact sheet is the one source, this is it spelled without the import.
95
+ const PORTS: utinyint = #fact(input.joysticks);
96
+ }
97
+
98
+ export namespace joystick {
99
+ // Read every port this machine has into the snapshot. Once a frame,
100
+ // right after waitFrame().
101
+ //
102
+ // It also zeroes ATRACT ($4D), and that is not a side errand. The OS
103
+ // counts that byte up in its VBI and, after roughly nine minutes with
104
+ // no key pressed, starts cycling the playfield colours to save the
105
+ // phosphor — which a keyboard game never sees, because the OS clears
106
+ // ATRACT on a keypress, and a joystick-only game always does, because
107
+ // a joystick is not a key. Left alone it turns a working game's screen
108
+ // into slowly rolling colours partway through a long session. Three
109
+ // bytes here is the whole fix, and this is the one function a
110
+ // joystick-only game is certain to call every frame.
111
+ function scan(): void {
112
+ for (let port: utinyint = 0; port < Joystick.PORTS; port++) {
113
+ let pushed: utinyint = (memory.read(STICK0 + port) ^ 0x0F) & 0x0F;
114
+ // TRIG is 0 while the button is down.
115
+ if (memory.read(STRIG0 + port) == 0) {
116
+ pushed = pushed + Joystick.FIRE;
117
+ }
118
+ state[port] = pushed;
119
+ }
120
+ attract = 0;
121
+ }
122
+
123
+ // The five switches of one port as `Joystick.*` bits, 1 = pushed.
124
+ function bits(port: utinyint): utinyint {
125
+ return state[port];
126
+ }
127
+
128
+ function up(port: utinyint): bool {
129
+ return (state[port] & Joystick.UP) != 0;
130
+ }
131
+
132
+ function down(port: utinyint): bool {
133
+ return (state[port] & Joystick.DOWN) != 0;
134
+ }
135
+
136
+ function left(port: utinyint): bool {
137
+ return (state[port] & Joystick.LEFT) != 0;
138
+ }
139
+
140
+ function right(port: utinyint): bool {
141
+ return (state[port] & Joystick.RIGHT) != 0;
142
+ }
143
+
144
+ function fire(port: utinyint): bool {
145
+ return (state[port] & Joystick.FIRE) != 0;
146
+ }
147
+ }
@@ -0,0 +1,157 @@
1
+ // @8bitscript/atari8/keyboard — the keyboard, one snapshot a frame.
2
+ //
3
+ // Named by "8bitscript".exports["./keyboard"] in this package's package.json:
4
+ //
5
+ // import { keyboard } from "@8bitscript/atari8/keyboard";
6
+ // import { Key } from "@8bitscript/atari8/keys";
7
+ //
8
+ // keyboard.scan();
9
+ // if (keyboard.pressed(Key.SPACE)) { ... }
10
+ //
11
+ // Hardware-level, Atari-only surface, the same shape as
12
+ // @8bitscript/c64/keyboard and @8bitscript/pet/keyboard — `scan()` once a
13
+ // frame, then questions answered from the snapshot. What is behind it is
14
+ // not the same shape at all, and the difference is the whole point of this
15
+ // file, so read the next paragraph before writing a game around it.
16
+ //
17
+ // ---- this machine cannot tell you which keys are down ----------------------
18
+ //
19
+ // The C64 and PET have a matrix a program drives itself: select a column,
20
+ // read eight rows, and after eight passes you know the state of every key at
21
+ // once. The Atari has no matrix a program can reach. POKEY scans the
22
+ // keyboard in hardware and reports *one* thing — the code of the last key
23
+ // it saw, in KBCODE ($D209), with an IRQ to say it changed — plus one bit in
24
+ // SKSTAT saying whether a key, that key, is still held. So:
25
+ //
26
+ // - "is SPACE down" is answerable: the last key was SPACE and a key is
27
+ // still held. `pressed()` is exactly that, and it is honest.
28
+ // - "are UP and FIRE both down" is *not* answerable here. Two keys at once
29
+ // is what the joystick is for on this machine (./joystick.8bs), and it
30
+ // is why Atari games put movement on a stick and reserve the keyboard
31
+ // for one-at-a-time choices. A portable input capability must not
32
+ // promise chords on this target.
33
+ // - a key held down repeats: the OS's VBI auto-repeats it into CH after a
34
+ // delay, so `key()` fires again. `pressed()` does not care, but a menu
35
+ // that steps on every `key()` will step twice.
36
+ //
37
+ // ---- why this reads the OS shadow, not KBCODE ------------------------------
38
+ //
39
+ // The same reasoning as ./joystick.8bs. POKEY's keyboard IRQ is live under an
40
+ // 8BitScript build (no `sei` is emitted; see ../AGENTS.md), and the OS's
41
+ // handler is what debounces the key, applies SHIFT and CTRL, handles the
42
+ // auto-repeat and the CAPS lock, and leaves the result in CH ($02FC), $FF
43
+ // for "nothing new". Reading KBCODE instead means racing that handler for a
44
+ // register it is already consuming, and re-implementing debounce. So
45
+ // `scan()` takes CH and hands it back to the OS as $FF — the documented way
46
+ // a program consumes a key — and only the "still held" bit comes from the
47
+ // hardware, because the OS keeps no shadow of it.
48
+ //
49
+ // A program that has taken the machine over (NMIEN's VBI bit cleared) has no
50
+ // CH being filled and must read KBCODE itself; `keyboard.raw()` is that
51
+ // register, for exactly that case.
52
+ import { chShadow, skstat, kbcode } from "./index.8bs";
53
+
54
+ // The last code `scan()` took from CH, modifiers included, or `Key.NONE`.
55
+ let last: utinyint = 0xFF;
56
+ // Whether a key was still held at the last scan.
57
+ let held: bool = false;
58
+ // Whether SHIFT was down at the last scan, from SKSTAT rather than from the
59
+ // code's bit 6 — so it is true while SHIFT is held on its own.
60
+ let shifted: bool = false;
61
+
62
+ // SKSTAT's keyboard bits are active low: the bit is 0 while the thing is
63
+ // true. (Atari Hardware Manual; the SDK's _pokey.h names them
64
+ // SKSTAT_LASTKEY_PRESSED and SKSTAT_SHIFTKEY_PRESSED without the polarity.
65
+ // Checked here under atari800 with nothing pressed — both bits read 1 — by
66
+ // test/layers-probe.8bs.)
67
+ const SK_KEY_HELD: utinyint = 0x04;
68
+ const SK_SHIFT_HELD: utinyint = 0x08;
69
+
70
+ export namespace keyboard {
71
+ // Take whatever the OS has, once a frame, right after waitFrame().
72
+ //
73
+ // A key is consumed here: CH goes back to $FF so the next press is seen
74
+ // as new. That means exactly one `scan()` per frame — a second call in
75
+ // the same frame finds nothing and clears the answer.
76
+ function scan(): void {
77
+ let code: utinyint = chShadow;
78
+ if (code != 0xFF) {
79
+ last = code;
80
+ chShadow = 0xFF;
81
+ } else {
82
+ // No new key. Keep the old code only while it is still held, so
83
+ // `pressed()` follows the key down and up rather than latching
84
+ // the last key pressed forever.
85
+ let status: utinyint = skstat;
86
+ if ((status & SK_KEY_HELD) != 0) {
87
+ last = 0xFF;
88
+ }
89
+ }
90
+ let status2: utinyint = skstat;
91
+ held = (status2 & SK_KEY_HELD) == 0;
92
+ shifted = (status2 & SK_SHIFT_HELD) == 0;
93
+ }
94
+
95
+ // Is this key the one being pressed? `key` is a `Key.*` code; SHIFT and
96
+ // CTRL are masked off both sides, so `pressed(Key.A)` is true for a
97
+ // shifted A as well. To require a modifier, compare `code()` against
98
+ // `Key.A + Key.SHIFT` instead.
99
+ //
100
+ // The `Key.NONE` guard is not belt-and-braces: `Key.NONE` is $FF and the
101
+ // mask is $3F, so masking an idle keyboard gives $3F — which is exactly
102
+ // `Key.A`. Without this line an untouched machine reports A held down
103
+ // forever, and it looks perfectly correct in source. test/layers-probe.8bs
104
+ // checks this one case in its idle reading for that reason.
105
+ function pressed(key: utinyint): bool {
106
+ if (last == 0xFF) {
107
+ return false;
108
+ }
109
+ return (last & 0x3F) == (key & 0x3F);
110
+ }
111
+
112
+ // The last key's code with its modifiers masked off, or `Key.NONE`.
113
+ function key(): utinyint {
114
+ if (last == 0xFF) {
115
+ return 0xFF;
116
+ }
117
+ return last & 0x3F;
118
+ }
119
+
120
+ // The last key's code exactly as it arrived, `Key.SHIFT` and `Key.CTRL`
121
+ // still on it, or `Key.NONE`.
122
+ function code(): utinyint {
123
+ return last;
124
+ }
125
+
126
+ // Is a key — the one `key()` names — still being held down?
127
+ function down(): bool {
128
+ return held;
129
+ }
130
+
131
+ // Is SHIFT held, whether or not any other key is? This is SKSTAT's own
132
+ // bit, so it is true for SHIFT alone, which the key code cannot say.
133
+ function shift(): bool {
134
+ return shifted;
135
+ }
136
+
137
+ // Was CTRL held when the last key arrived? CTRL alone cannot be read on
138
+ // this machine at all — there is no bit for it — so this is the code's
139
+ // modifier and nothing more.
140
+ function control(): bool {
141
+ return (last & 0x80) != 0;
142
+ }
143
+
144
+ // Forget the last key, without waiting for a scan: a menu that has acted
145
+ // on a choice and does not want it acted on twice.
146
+ function clear(): void {
147
+ last = 0xFF;
148
+ chShadow = 0xFF;
149
+ }
150
+
151
+ // POKEY's own KBCODE, unfiltered and undebounced — for a program that
152
+ // has cleared NMIEN's VBI bit and so has no OS filling CH. Nothing else
153
+ // in this file reads it.
154
+ function raw(): utinyint {
155
+ return kbcode;
156
+ }
157
+ }
package/src/keys.8bs ADDED
@@ -0,0 +1,116 @@
1
+ // @8bitscript/atari8/keys — the keyboard's codes, key by key.
2
+ //
3
+ // Named by "8bitscript".exports["./keys"] in this package's package.json, for
4
+ //
5
+ // import { keyboard } from "@8bitscript/atari8/keyboard";
6
+ // import { Key } from "@8bitscript/atari8/keys";
7
+ //
8
+ // if (keyboard.pressed(Key.SPACE)) { ... }
9
+ //
10
+ // Each value is the code POKEY leaves in KBCODE ($D209) and the OS leaves in
11
+ // CH ($02FC) — not a row and column the way the C64's and PET's tables are,
12
+ // because on this machine a program never scans a matrix. POKEY scans it in
13
+ // hardware and hands over one code (see ./keyboard.8bs for what that costs).
14
+ //
15
+ // The numbering looks arbitrary because it is: it is the order POKEY's own
16
+ // scan counter reaches each key, so `L` is 0 and `A` is $3F with nothing in
17
+ // between to reason about. Bits 6 and 7 are the SHIFT and CTRL modifiers
18
+ // OR-ed onto the key's own six bits, which is why every code here is under
19
+ // $40 and why `Key.SHIFT` and `Key.CTRL` are masks rather than keys.
20
+ //
21
+ // A key that also exists on the C64 and PET has the same name here —
22
+ // letters, `DIGIT_n`, SPACE, RETURN, DELETE, ESC, the punctuation — so a
23
+ // program that sticks to those names reads the same on all three machines'
24
+ // keyboard layers.
25
+ //
26
+ // Source: `KEY_*` in the LLVM-MOS SDK's own
27
+ // `mos-platform/atari8-common/include/atari.h`, which is also mirrored as
28
+ // assembler equates in `asminc/atari.inc`; this file is generated from that
29
+ // header, not transcribed, and packages/atari8/test/keys.test.mjs checks
30
+ // every value against it whenever the SDK is present. (../AGENTS.md listed
31
+ // this table as "to verify" before the header was found; it is now sourced.)
32
+
33
+ export namespace Key {
34
+ // Nothing pressed: what CH holds when the OS has no key for the program,
35
+ // and what `keyboard.key()` answers.
36
+ const NONE: utinyint = 0xFF;
37
+
38
+ // Letters.
39
+ const A: utinyint = 0x3F;
40
+ const B: utinyint = 0x15;
41
+ const C: utinyint = 0x12;
42
+ const D: utinyint = 0x3A;
43
+ const E: utinyint = 0x2A;
44
+ const F: utinyint = 0x38;
45
+ const G: utinyint = 0x3D;
46
+ const H: utinyint = 0x39;
47
+ const I: utinyint = 0x0D;
48
+ const J: utinyint = 0x01;
49
+ const K: utinyint = 0x05;
50
+ const L: utinyint = 0x00;
51
+ const M: utinyint = 0x25;
52
+ const N: utinyint = 0x23;
53
+ const O: utinyint = 0x08;
54
+ const P: utinyint = 0x0A;
55
+ const Q: utinyint = 0x2F;
56
+ const R: utinyint = 0x28;
57
+ const S: utinyint = 0x3E;
58
+ const T: utinyint = 0x2D;
59
+ const U: utinyint = 0x0B;
60
+ const V: utinyint = 0x10;
61
+ const W: utinyint = 0x2E;
62
+ const X: utinyint = 0x16;
63
+ const Y: utinyint = 0x2B;
64
+ const Z: utinyint = 0x17;
65
+
66
+ // Digits, named DIGIT_n so a name never starts with a number.
67
+ const DIGIT_0: utinyint = 0x32;
68
+ const DIGIT_1: utinyint = 0x1F;
69
+ const DIGIT_2: utinyint = 0x1E;
70
+ const DIGIT_3: utinyint = 0x1A;
71
+ const DIGIT_4: utinyint = 0x18;
72
+ const DIGIT_5: utinyint = 0x1D;
73
+ const DIGIT_6: utinyint = 0x1B;
74
+ const DIGIT_7: utinyint = 0x33;
75
+ const DIGIT_8: utinyint = 0x35;
76
+ const DIGIT_9: utinyint = 0x30;
77
+
78
+ // Punctuation, unshifted. The shifted character on the same key is
79
+ // that key OR Key.SHIFT.
80
+ const COMMA: utinyint = 0x20;
81
+ const PERIOD: utinyint = 0x22;
82
+ const SLASH: utinyint = 0x26;
83
+ const SEMICOLON: utinyint = 0x02;
84
+ const PLUS: utinyint = 0x06; // '+', shifted '\\'
85
+ const ASTERISK: utinyint = 0x07; // '*'
86
+ const DASH: utinyint = 0x0E; // '-'
87
+ const EQUALS: utinyint = 0x0F; // '='
88
+ const LESSTHAN: utinyint = 0x36; // '<', on its own key
89
+ const GREATERTHAN: utinyint = 0x37; // '>', on its own key
90
+
91
+ // The keys that are not characters.
92
+ const ESC: utinyint = 0x1C;
93
+ const TAB: utinyint = 0x2C;
94
+ const SPACE: utinyint = 0x21;
95
+ const RETURN: utinyint = 0x0C;
96
+ const DELETE: utinyint = 0x34; // DELETE/BACK S
97
+ const CAPS: utinyint = 0x3C; // CAPS/LOWR
98
+ const INVERSE: utinyint = 0x27; // the Atari-logo key: inverse video
99
+ const HELP: utinyint = 0x11; // the 1200XL and later only
100
+
101
+ // Function keys: the 1200XL had four; no other model has any.
102
+ const F1: utinyint = 0x03;
103
+ const F2: utinyint = 0x04;
104
+ const F3: utinyint = 0x13;
105
+ const F4: utinyint = 0x14;
106
+
107
+ // Modifiers, as masks OR-ed onto a key code rather than keys of their
108
+ // own. Neither can be read alone through KBCODE: SHIFT on its own is
109
+ // SKSTAT's bit 3 (`keyboard.shift()`), and CTRL on its own cannot be
110
+ // read at all. There is no way to tell left SHIFT from right SHIFT.
111
+ const SHIFT: utinyint = 0x40;
112
+ const CTRL: utinyint = 0x80;
113
+ // The key itself, with both modifiers masked off — what
114
+ // `keyboard.key()` is compared against.
115
+ const CODE: utinyint = 0x3F;
116
+ }
@@ -0,0 +1,39 @@
1
+ // @8bitscript/atari8/pointer — the Atari 8-bit behind @8bitscript/pointer.
2
+ //
3
+ // Every call here does nothing and `DRAWS` is false, and both halves are
4
+ // missing for the same reason: nobody has written the drivers.
5
+ //
6
+ // **The hardware could do it.** GTIA has four players and four missiles —
7
+ // movable objects a cursor fits in, though a player is 8 pixels wide and
8
+ // as tall as the display, so an arrow is a few bytes of a player's stripe
9
+ // rather than a 24x21 block like the VIC's sprite. And the catalog already
10
+ // offers a pointing device: `mouse` with `st`, `amiga` and `trak` values,
11
+ // each of which atari800 emulates on `-mouseport`.
12
+ //
13
+ // **Neither end is built.** This package has no players/missiles layer for
14
+ // the drawing, and @8bitscript/atari8/input reports no pointer because an
15
+ // ST or Amiga mouse on this machine is quadrature on a joystick port's
16
+ // direction bits, decoded in software at a high poll rate — a driver in
17
+ // its own right, listed as a gap in packages/input/AGENTS.md. An arrow
18
+ // wants the input half first: there is no point drawing a cursor with
19
+ // nothing to move it.
20
+ //
21
+ // It costs the machine nothing meanwhile: `DRAWS` is a const and every
22
+ // function is empty, so a program's `if (pointer.DRAWS)` folds away and
23
+ // LLVM deletes the calls.
24
+ export namespace pointer {
25
+
26
+ const DRAWS: bool = false;
27
+
28
+ function begin(): void {
29
+ }
30
+
31
+ function setColor(color: utinyint): void {
32
+ }
33
+
34
+ function update(): void {
35
+ }
36
+
37
+ function hide(): void {
38
+ }
39
+ }