@8bitscript/c64 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.
- package/LICENSE +21 -0
- package/native/6502/raster.s +121 -0
- package/package.json +396 -0
- package/src/bitmap.8bs +175 -0
- package/src/charset.8bs +142 -0
- package/src/geometry.8bs +116 -0
- package/src/index.8bs +427 -0
- package/src/input.8bs +269 -0
- package/src/joystick.8bs +78 -0
- package/src/keyboard.8bs +84 -0
- package/src/keys.8bs +103 -0
- package/src/mouse.8bs +265 -0
- package/src/pointer.8bs +183 -0
- package/src/random.8bs +81 -0
- package/src/raster.8bs +190 -0
- package/src/reu.8bs +210 -0
- package/src/screen.8bs +120 -0
- package/src/scroll.8bs +130 -0
- package/src/sid.8bs +211 -0
- package/src/sprites.8bs +197 -0
- package/src/text.8bs +174 -0
package/src/input.8bs
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// @8bitscript/c64/input — the C64 behind @8bitscript/input.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./input"] in this package's package.json,
|
|
4
|
+
// and by "8bitscript".entry.c64 in @8bitscript/input's, so a portable
|
|
5
|
+
// program writes
|
|
6
|
+
//
|
|
7
|
+
// import { input } from "@8bitscript/input";
|
|
8
|
+
//
|
|
9
|
+
// and gets this file on a C64. Nothing here is C64-specific to a program:
|
|
10
|
+
// it is the same ten calls on all nine machines, answered from whatever
|
|
11
|
+
// this one has. The machine-level layers underneath — ./keyboard.8bs,
|
|
12
|
+
// ./keys.8bs, ./joystick.8bs, ./mouse.8bs — stay available to a program
|
|
13
|
+
// that wants the matrix itself.
|
|
14
|
+
//
|
|
15
|
+
// ---- what a direction means here ------------------------------------------
|
|
16
|
+
//
|
|
17
|
+
// A menu asks "did the user just press right", not "is right held down",
|
|
18
|
+
// so every direction and button here is **edge-triggered**: true on the
|
|
19
|
+
// one frame the press begins, false while it is held. That is what makes
|
|
20
|
+
// a highlight step one item per press instead of racing across the bar,
|
|
21
|
+
// and it is why `poll()` has to be called exactly once a frame — twice a
|
|
22
|
+
// frame and every press is seen once and then swallowed.
|
|
23
|
+
//
|
|
24
|
+
// Three things can move the highlight on this machine, and all three are
|
|
25
|
+
// or'd together, because a program should not care which the user reached
|
|
26
|
+
// for:
|
|
27
|
+
//
|
|
28
|
+
// - **The cursor keys.** The C64 has two, not four: `CURSOR_RIGHT` and
|
|
29
|
+
// `CURSOR_DOWN`, with SHIFT reversing each. So left is SHIFT plus the
|
|
30
|
+
// right key, which is what the machine actually has and what this file
|
|
31
|
+
// reports rather than pretending there are four keys.
|
|
32
|
+
// - **A joystick in port 2**, which is where every C64 game asked for
|
|
33
|
+
// one. Port 1 shares wires with the keyboard's rows, so a stick there
|
|
34
|
+
// types; port 2 is the one that does not fight the matrix.
|
|
35
|
+
// - **A 1351 mouse in port 1**, if this build was fitted with one — see
|
|
36
|
+
// below.
|
|
37
|
+
//
|
|
38
|
+
// RETURN or the joystick's fire button confirms; RUN/STOP cancels.
|
|
39
|
+
//
|
|
40
|
+
// ---- the mouse is a compile-time decision and a run-time answer -----------
|
|
41
|
+
//
|
|
42
|
+
// `#fact(input.mouse)` is false on a stock C64 and true for a build made
|
|
43
|
+
// with `--hardware port1=mouse1351`. It is a `run` fact, so what it means
|
|
44
|
+
// is "this build may use a mouse" — and that is exactly the two-stage
|
|
45
|
+
// shape this machine needs:
|
|
46
|
+
//
|
|
47
|
+
// - **Compiling.** `HAS_MOUSE` is a const, so `if (HAS_MOUSE)` is
|
|
48
|
+
// `if (false)` on a stock build and the pointer code below is never
|
|
49
|
+
// reached, never referenced, and never linked. A program that did not
|
|
50
|
+
// ask for a mouse pays nothing for one — not a byte of code, not a
|
|
51
|
+
// byte of the accumulator.
|
|
52
|
+
// - **Running.** A 1351 says nothing about itself except through the
|
|
53
|
+
// lines its movement arrives on, so even a build that asked for one
|
|
54
|
+
// has to find out whether one is plugged in. `pointer()` answers that,
|
|
55
|
+
// from ./mouse.8bs's probe, every frame.
|
|
56
|
+
//
|
|
57
|
+
// **The port assignment is this file's choice, not a fact.**
|
|
58
|
+
// `input.mouse` is one flag and does not say which port the mouse is in.
|
|
59
|
+
// This file reads the mouse in **port 1** and the joystick in **port 2**,
|
|
60
|
+
// which is the arrangement C64 software settled on — a stick in the port
|
|
61
|
+
// that does not fight the keyboard, a pointer in the one that does. A
|
|
62
|
+
// program that needs it the other way round wants ./mouse.8bs directly.
|
|
63
|
+
//
|
|
64
|
+
// **The pointer is reported in cells, not pixels**, because that is the
|
|
65
|
+
// unit @8bitscript/text works in and the unit a component hit-tests
|
|
66
|
+
// against: `pointerCell()` is a flat cell index, the same number
|
|
67
|
+
// `text.print()` takes. A 1351 count is about a pixel, so the accumulator
|
|
68
|
+
// is limited to the screen in pixels and shifted down by the cell size.
|
|
69
|
+
//
|
|
70
|
+
// **Y is inverted in ./mouse.8bs**, not here. A 1351's Y counter runs
|
|
71
|
+
// opposite the screen; `poll()` there swaps last and now on that axis so
|
|
72
|
+
// `mouse.y()` is already in screen rows (0 at the top). This file only
|
|
73
|
+
// shifts that number to a cell. Verified under x64sc (Studio, 2026-09-07):
|
|
74
|
+
// applying Y with the same sign as X moved the arrow up when the mouse
|
|
75
|
+
// moved down; inverting the delta, not `LIMIT_Y - y`, kept the rest
|
|
76
|
+
// position in the top-left corner.
|
|
77
|
+
import { Video } from "./geometry.8bs";
|
|
78
|
+
import { keyboard } from "./keyboard.8bs";
|
|
79
|
+
import { Key } from "./keys.8bs";
|
|
80
|
+
import { joystick, Joystick } from "./joystick.8bs";
|
|
81
|
+
import { mouse, Mouse } from "./mouse.8bs";
|
|
82
|
+
|
|
83
|
+
// Was this build fitted with a mouse? Folded while compiling, so the whole
|
|
84
|
+
// pointer half of this file is either present or absent, never tested.
|
|
85
|
+
const HAS_MOUSE: bool = #fact(input.mouse);
|
|
86
|
+
|
|
87
|
+
// The pointer's travel, in mouse counts — a count is about a pixel, and a
|
|
88
|
+
// cell is 8 of them on this machine. The shift is written as a shift
|
|
89
|
+
// rather than a divide by CELL_PIXELS because a 6502 has neither, and the
|
|
90
|
+
// one the compiler can do without a helper routine is this one.
|
|
91
|
+
const CELL_SHIFT: utinyint = 3; // 8 pixels a cell
|
|
92
|
+
const LIMIT_X: usmallint = 319; // Video.COLUMNS * 8 - 1
|
|
93
|
+
const LIMIT_Y: usmallint = 199; // Video.ROWS * 8 - 1
|
|
94
|
+
|
|
95
|
+
// One bit per thing a program can ask about, so a frame's input is one
|
|
96
|
+
// byte and an edge is one AND.
|
|
97
|
+
namespace Edge {
|
|
98
|
+
const LEFT: utinyint = 1;
|
|
99
|
+
const RIGHT: utinyint = 2;
|
|
100
|
+
const UP: utinyint = 4;
|
|
101
|
+
const DOWN: utinyint = 8;
|
|
102
|
+
const CONFIRM: utinyint = 16;
|
|
103
|
+
const CANCEL: utinyint = 32;
|
|
104
|
+
const BUTTON: utinyint = 64;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// What is held now, what was held at the last poll, and what began this
|
|
108
|
+
// frame — `now & ~last`, written with the XOR the language has.
|
|
109
|
+
let held: utinyint = 0;
|
|
110
|
+
let before: utinyint = 0;
|
|
111
|
+
let began: utinyint = 0;
|
|
112
|
+
|
|
113
|
+
// Where the pointer is, in cells, and whether one is really there.
|
|
114
|
+
let atCell: usmallint = 0;
|
|
115
|
+
let havePointer: bool = false;
|
|
116
|
+
|
|
117
|
+
export namespace input {
|
|
118
|
+
|
|
119
|
+
// Once, at start-up, before the first poll() — the input equivalent of
|
|
120
|
+
// screen.blank(). On this machine it gives the pointer its ceiling and
|
|
121
|
+
// points the SID at the mouse's port so the first poll() has something
|
|
122
|
+
// settled to read. On a build with no mouse it compiles to nothing at
|
|
123
|
+
// all, and on a machine with no pointer it is an empty function that
|
|
124
|
+
// costs a program one call it can write once and forget.
|
|
125
|
+
function begin(): void {
|
|
126
|
+
if (HAS_MOUSE) {
|
|
127
|
+
mouse.setLimits(LIMIT_X, LIMIT_Y);
|
|
128
|
+
mouse.select(Mouse.PORT_1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Everything this machine offers, gathered once. Call it once a frame,
|
|
133
|
+
// right after waitFrame(), and never twice.
|
|
134
|
+
//
|
|
135
|
+
// The order is forced by the hardware and is the whole reason this
|
|
136
|
+
// function exists rather than three calls in a program:
|
|
137
|
+
//
|
|
138
|
+
// 1. **Read the pots first.** They answer for the port `select()`
|
|
139
|
+
// pointed the SID at during the *previous* poll — the converter
|
|
140
|
+
// needs about half a millisecond to settle, and a frame is thirty
|
|
141
|
+
// times that, so last frame's select is exactly what makes this
|
|
142
|
+
// frame's reading mean something.
|
|
143
|
+
// 2. **Then the keyboard**, which drives all eight column lines and
|
|
144
|
+
// leaves port A at $FF.
|
|
145
|
+
// 3. **Then the joysticks**, which need port A at $FF to read as
|
|
146
|
+
// joysticks — which is the state the keyboard scan just left.
|
|
147
|
+
// 4. **Then point the SID back at the mouse's port**, for the read
|
|
148
|
+
// at the top of the next frame.
|
|
149
|
+
function poll(): void {
|
|
150
|
+
if (HAS_MOUSE) {
|
|
151
|
+
mouse.poll();
|
|
152
|
+
}
|
|
153
|
+
keyboard.scan();
|
|
154
|
+
joystick.scan();
|
|
155
|
+
|
|
156
|
+
before = held;
|
|
157
|
+
held = 0;
|
|
158
|
+
|
|
159
|
+
// The two cursor keys and SHIFT, which is what "four directions"
|
|
160
|
+
// is on this keyboard.
|
|
161
|
+
let shifted: bool = keyboard.pressed(Key.SHIFT_LEFT) || keyboard.pressed(Key.SHIFT_RIGHT);
|
|
162
|
+
if (keyboard.pressed(Key.CURSOR_RIGHT)) {
|
|
163
|
+
if (shifted) {
|
|
164
|
+
held = held | Edge.LEFT;
|
|
165
|
+
} else {
|
|
166
|
+
held = held | Edge.RIGHT;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (keyboard.pressed(Key.CURSOR_DOWN)) {
|
|
170
|
+
if (shifted) {
|
|
171
|
+
held = held | Edge.UP;
|
|
172
|
+
} else {
|
|
173
|
+
held = held | Edge.DOWN;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (keyboard.pressed(Key.RETURN)) {
|
|
177
|
+
held = held | Edge.CONFIRM;
|
|
178
|
+
}
|
|
179
|
+
if (keyboard.pressed(Key.STOP)) {
|
|
180
|
+
held = held | Edge.CANCEL;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The stick in port 2, or'd on top: same directions, same confirm.
|
|
184
|
+
let stick: utinyint = joystick.bits(Joystick.PORT_2);
|
|
185
|
+
if ((stick & Joystick.LEFT) != 0) {
|
|
186
|
+
held = held | Edge.LEFT;
|
|
187
|
+
}
|
|
188
|
+
if ((stick & Joystick.RIGHT) != 0) {
|
|
189
|
+
held = held | Edge.RIGHT;
|
|
190
|
+
}
|
|
191
|
+
if ((stick & Joystick.UP) != 0) {
|
|
192
|
+
held = held | Edge.UP;
|
|
193
|
+
}
|
|
194
|
+
if ((stick & Joystick.DOWN) != 0) {
|
|
195
|
+
held = held | Edge.DOWN;
|
|
196
|
+
}
|
|
197
|
+
if ((stick & Joystick.FIRE) != 0) {
|
|
198
|
+
held = held | Edge.CONFIRM;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (HAS_MOUSE) {
|
|
202
|
+
havePointer = mouse.present();
|
|
203
|
+
if (havePointer) {
|
|
204
|
+
// Counts to cells. Y is already screen-space: ./mouse.8bs
|
|
205
|
+
// inverts the 1351's counter so 0 is the top row.
|
|
206
|
+
let column: usmallint = mouse.x() >> CELL_SHIFT;
|
|
207
|
+
let row: usmallint = mouse.y() >> CELL_SHIFT;
|
|
208
|
+
atCell = row * Video.COLUMNS + column;
|
|
209
|
+
}
|
|
210
|
+
// The mouse's buttons ride the port's ordinary joystick lines,
|
|
211
|
+
// so they are already in the snapshot taken above — read from
|
|
212
|
+
// there rather than from the live port, which by now has a
|
|
213
|
+
// keyboard column driven on it.
|
|
214
|
+
if ((joystick.bits(Joystick.PORT_1) & Joystick.FIRE) != 0) {
|
|
215
|
+
held = held | Edge.BUTTON;
|
|
216
|
+
}
|
|
217
|
+
// Point the SID at the mouse's port for the next frame's read.
|
|
218
|
+
// Last, because joystick.scan() puts port A back to $FF.
|
|
219
|
+
mouse.select(Mouse.PORT_1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
began = held & (before ^ 0xFF);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// The four directions, each true only on the frame its press began.
|
|
226
|
+
function left(): bool {
|
|
227
|
+
return (began & Edge.LEFT) != 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function right(): bool {
|
|
231
|
+
return (began & Edge.RIGHT) != 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function up(): bool {
|
|
235
|
+
return (began & Edge.UP) != 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function down(): bool {
|
|
239
|
+
return (began & Edge.DOWN) != 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// RETURN, or the stick's fire button.
|
|
243
|
+
function confirm(): bool {
|
|
244
|
+
return (began & Edge.CONFIRM) != 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// RUN/STOP.
|
|
248
|
+
function cancel(): bool {
|
|
249
|
+
return (began & Edge.CANCEL) != 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Is a pointer really there? False on every stock build, where the
|
|
253
|
+
// question folds away entirely; on a build fitted with a mouse, this
|
|
254
|
+
// is ./mouse.8bs's probe answering every frame.
|
|
255
|
+
function pointer(): bool {
|
|
256
|
+
return havePointer;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Where it is, as a flat cell index — the same unit text.print() takes.
|
|
260
|
+
// Meaningless unless pointer() is true.
|
|
261
|
+
function pointerCell(): usmallint {
|
|
262
|
+
return atCell;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// The pointer's main button, on the frame its press began.
|
|
266
|
+
function pointerButton(): bool {
|
|
267
|
+
return (began & Edge.BUTTON) != 0;
|
|
268
|
+
}
|
|
269
|
+
}
|
package/src/joystick.8bs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// @8bitscript/c64/joystick — the two 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/c64/joystick";
|
|
6
|
+
//
|
|
7
|
+
// joystick.scan();
|
|
8
|
+
// if (joystick.left(Joystick.PORT_2)) { ... }
|
|
9
|
+
//
|
|
10
|
+
// Hardware-level, C64-only surface, like ./keyboard.8bs: the layer a
|
|
11
|
+
// portable input capability will sit on. A digital joystick is five
|
|
12
|
+
// switches to ground — up, down, left, right, fire — on CIA1: port 2 on
|
|
13
|
+
// port A ($DC00) bits 0-4, port 1 on port B ($DC01) bits 0-4, a 0 bit
|
|
14
|
+
// where a switch is closed. `scan()` reads both once a frame into a
|
|
15
|
+
// snapshot with the bits inverted (1 = pushed) and everything else answers
|
|
16
|
+
// from it.
|
|
17
|
+
//
|
|
18
|
+
// Port A has to be $FF for the read to mean the joystick: its bits are
|
|
19
|
+
// the keyboard's column selects, and a selected column's keys would show
|
|
20
|
+
// up in port B as joystick 1. `keyboard.scan()` leaves it so; `scan()`
|
|
21
|
+
// writes it again anyway, so a program with no keyboard is right too. The
|
|
22
|
+
// order once a frame is: waitFrame(), keyboard.scan(), joystick.scan().
|
|
23
|
+
//
|
|
24
|
+
// Port 2 is where a game reads its player, and where every C64 game asked
|
|
25
|
+
// for the stick: port 1 shares wires with the keyboard's rows, so a stick
|
|
26
|
+
// there "types" (the KERNAL's scan sees it — irrelevant here, interrupts
|
|
27
|
+
// are off) and, the other way round, keys held down read as joystick 1
|
|
28
|
+
// pushes. Port 2 shares the column lines, which a scan only drives for the
|
|
29
|
+
// length of `keyboard.scan()`. Paddles and the 1351 mouse are the SID's
|
|
30
|
+
// two A/D lines (`paddleX`/`paddleY` in ./index.8bs), not this file.
|
|
31
|
+
import { cia1PortA, cia1PortB } from "./index.8bs";
|
|
32
|
+
|
|
33
|
+
// The last scan: index 0 port 1, index 1 port 2; bits as in `Joystick`.
|
|
34
|
+
let state: array<utinyint, 2>;
|
|
35
|
+
|
|
36
|
+
export namespace Joystick {
|
|
37
|
+
const PORT_1: utinyint = 0;
|
|
38
|
+
const PORT_2: utinyint = 1;
|
|
39
|
+
const UP: utinyint = 1;
|
|
40
|
+
const DOWN: utinyint = 2;
|
|
41
|
+
const LEFT: utinyint = 4;
|
|
42
|
+
const RIGHT: utinyint = 8;
|
|
43
|
+
const FIRE: utinyint = 16;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export namespace joystick {
|
|
47
|
+
// Read both ports into the snapshot. Once a frame, after keyboard.scan().
|
|
48
|
+
function scan(): void {
|
|
49
|
+
cia1PortA = 0xFF;
|
|
50
|
+
state[0] = (cia1PortB ^ 0xFF) & 0x1F;
|
|
51
|
+
state[1] = (cia1PortA ^ 0xFF) & 0x1F;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The five switches of one port as `Joystick.*` bits, 1 = pushed.
|
|
55
|
+
function bits(port: utinyint): utinyint {
|
|
56
|
+
return state[port];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function up(port: utinyint): bool {
|
|
60
|
+
return (state[port] & Joystick.UP) != 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function down(port: utinyint): bool {
|
|
64
|
+
return (state[port] & Joystick.DOWN) != 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function left(port: utinyint): bool {
|
|
68
|
+
return (state[port] & Joystick.LEFT) != 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function right(port: utinyint): bool {
|
|
72
|
+
return (state[port] & Joystick.RIGHT) != 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function fire(port: utinyint): bool {
|
|
76
|
+
return (state[port] & Joystick.FIRE) != 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/keyboard.8bs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @8bitscript/c64/keyboard — the C64's keyboard matrix, one snapshot a frame.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./keyboard"] in this package's package.json:
|
|
4
|
+
//
|
|
5
|
+
// import { keyboard } from "@8bitscript/c64/keyboard";
|
|
6
|
+
// import { Key } from "@8bitscript/c64/keys";
|
|
7
|
+
//
|
|
8
|
+
// This is hardware-level, C64-only surface — the layer the portable input
|
|
9
|
+
// capability is built on: `@8bitscript/c64/input` reads this file and
|
|
10
|
+
// @8bitscript/input resolves to that, the way ./text.8bs is the layer
|
|
11
|
+
// under @8bitscript/text. Importing it makes a program C64-specific.
|
|
12
|
+
// It keeps no key buffer and decodes no PETSCII; it answers "is this key
|
|
13
|
+
// down now", which is what a game loop asks.
|
|
14
|
+
//
|
|
15
|
+
// ---- how the matrix works -----------------------------------------------
|
|
16
|
+
//
|
|
17
|
+
// Eight columns by eight rows on CIA1, no diodes. Writing a byte with one
|
|
18
|
+
// 0 bit to port A ($DC00) selects that column; port B ($DC01) then reads
|
|
19
|
+
// the column's eight keys, a 0 bit where a key is down. The KERNAL's own
|
|
20
|
+
// scan (SCNKEY, from its 60 Hz timer IRQ) drives the same two ports, and
|
|
21
|
+
// would land its column select between this scan's write and read — which
|
|
22
|
+
// is one reason the frame runtime starts a C64 program with `sei`
|
|
23
|
+
// (FRAME_SYNC.c64 in packages/backend-6502): from the first waitFrame()
|
|
24
|
+
// on, only this program touches CIA1. `scan()` writes the two direction
|
|
25
|
+
// registers itself (A out, B in — the KERNAL's setting, made sure of), so
|
|
26
|
+
// it does not depend on what ran before.
|
|
27
|
+
//
|
|
28
|
+
// `scan()` reads all eight columns into `state`, once a frame, right after
|
|
29
|
+
// waitFrame() returns; every question after that reads the snapshot.
|
|
30
|
+
// Sixteen port accesses, well under 200 cycles of a 17000-cycle NTSC frame.
|
|
31
|
+
// It leaves port A at $FF — no column selected — which is the state
|
|
32
|
+
// @8bitscript/c64/joystick needs to read the ports as joysticks; call
|
|
33
|
+
// `keyboard.scan()` first and `joystick.scan()` second, both once a frame.
|
|
34
|
+
//
|
|
35
|
+
// The joystick lines are wired in parallel with the matrix: a stick in
|
|
36
|
+
// port 1 pulls rows (a pushed direction reads as a key in every column),
|
|
37
|
+
// a stick in port 2 pulls columns (an extra column selected, its keys
|
|
38
|
+
// showing up under another's). A program that uses the keyboard and a
|
|
39
|
+
// joystick at once lives with that, as every C64 game did — port 2 for
|
|
40
|
+
// the stick, and keys that a stick cannot fake (a game that pauses on
|
|
41
|
+
// RUN/STOP should expect it while the port-1 stick is pushed up: row 7).
|
|
42
|
+
// Ghosting: three keys down can read as four (any three corners of a
|
|
43
|
+
// rectangle in the matrix light the fourth). Two keys are always safe.
|
|
44
|
+
import { cia1PortA, cia1PortB, cia1DirectionA, cia1DirectionB } from "./index.8bs";
|
|
45
|
+
|
|
46
|
+
// The last scan, one byte per column, a 1 bit where a key is down (the
|
|
47
|
+
// port's active-low rows inverted once, here, so nothing else has to
|
|
48
|
+
// think about it). All zero until the first scan.
|
|
49
|
+
let state: array<utinyint, 8>;
|
|
50
|
+
|
|
51
|
+
// The column-select byte for each column, and the row masks — indexed
|
|
52
|
+
// rather than shifted, as `1 << n` with a variable n is a loop on the 6502.
|
|
53
|
+
const COLUMN_SELECT: array<utinyint, 8> = [254, 253, 251, 247, 239, 223, 191, 127];
|
|
54
|
+
const ROW_BIT: array<utinyint, 8> = [1, 2, 4, 8, 16, 32, 64, 128];
|
|
55
|
+
|
|
56
|
+
export namespace keyboard {
|
|
57
|
+
const COLUMNS: utinyint = 8;
|
|
58
|
+
const ROWS: utinyint = 8;
|
|
59
|
+
|
|
60
|
+
// Read all eight columns into the snapshot. Once a frame, right after
|
|
61
|
+
// waitFrame(), before joystick.scan().
|
|
62
|
+
function scan(): void {
|
|
63
|
+
cia1DirectionA = 0xFF;
|
|
64
|
+
cia1DirectionB = 0x00;
|
|
65
|
+
for (let column: utinyint = 0; column < 8; column++) {
|
|
66
|
+
cia1PortA = COLUMN_SELECT[column];
|
|
67
|
+
state[column] = cia1PortB ^ 0xFF;
|
|
68
|
+
}
|
|
69
|
+
cia1PortA = 0xFF;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Is this key down in the last scan? `key` is a `Key.*` value from
|
|
73
|
+
// @8bitscript/c64/keys — `column * 8 + row` in the matrix.
|
|
74
|
+
function pressed(key: utinyint): bool {
|
|
75
|
+
return (state[key >> 3] & ROW_BIT[key & 7]) != 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The last scan's eight rows of one column, a 1 bit per key down:
|
|
79
|
+
// for code that wants a whole column at once (an "any key" test over
|
|
80
|
+
// all eight, a ghosting check).
|
|
81
|
+
function column(index: utinyint): utinyint {
|
|
82
|
+
return state[index];
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/keys.8bs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @8bitscript/c64/keys — the keyboard matrix, key by key.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./keys"] in this package's package.json, for
|
|
4
|
+
//
|
|
5
|
+
// import { keyboard } from "@8bitscript/c64/keyboard";
|
|
6
|
+
// import { Key } from "@8bitscript/c64/keys";
|
|
7
|
+
//
|
|
8
|
+
// if (keyboard.pressed(Key.SPACE)) { ... }
|
|
9
|
+
//
|
|
10
|
+
// Each value is `column * 8 + row` in CIA1's matrix (see ./keyboard.8bs):
|
|
11
|
+
// the column is the bit of port A ($DC00) driven low to select it, the row
|
|
12
|
+
// the bit of port B ($DC01) that reads low. Every C64 ever made has this
|
|
13
|
+
// one matrix — the C64C, SX-64, and the rest moved keys on the case, not
|
|
14
|
+
// on the wires — so unlike the PET there is one table and no profile twin.
|
|
15
|
+
// Two keys are not in it: RESTORE is wired to the CPU's NMI line, and
|
|
16
|
+
// SHIFT LOCK is the left SHIFT key's own wire, held down.
|
|
17
|
+
//
|
|
18
|
+
// A key that also exists on the PET has the same name here — letters,
|
|
19
|
+
// `DIGIT_n`, SPACE, RETURN, STOP, HOME, DELETE, the cursor keys, both
|
|
20
|
+
// shifts, the arrows and the shared punctuation — so a program that
|
|
21
|
+
// sticks to those names reads the same on both machines' keyboard layers.
|
|
22
|
+
//
|
|
23
|
+
// Source: the matrix in the Programmer's Reference Guide and every C64
|
|
24
|
+
// reference since, checked here key for key against VICE's own positional
|
|
25
|
+
// keyboard map for the C64, /opt/homebrew/share/vice/C64/gtk3_pos.vkm
|
|
26
|
+
// (each line there is `keysym column row shiftflag`, in the same
|
|
27
|
+
// numbering as this file); packages/compiler/test/c64-keys.test.mjs repeats
|
|
28
|
+
// that check when the file is present.
|
|
29
|
+
|
|
30
|
+
export namespace Key {
|
|
31
|
+
// column 0
|
|
32
|
+
const DELETE: utinyint = 0; // INST/DEL
|
|
33
|
+
const RETURN: utinyint = 1;
|
|
34
|
+
const CURSOR_RIGHT: utinyint = 2; // shifted: cursor left
|
|
35
|
+
const F7: utinyint = 3; // shifted: F8
|
|
36
|
+
const F1: utinyint = 4; // shifted: F2
|
|
37
|
+
const F3: utinyint = 5; // shifted: F4
|
|
38
|
+
const F5: utinyint = 6; // shifted: F6
|
|
39
|
+
const CURSOR_DOWN: utinyint = 7; // shifted: cursor up
|
|
40
|
+
// column 1
|
|
41
|
+
const DIGIT_3: utinyint = 8;
|
|
42
|
+
const W: utinyint = 9;
|
|
43
|
+
const A: utinyint = 10;
|
|
44
|
+
const DIGIT_4: utinyint = 11;
|
|
45
|
+
const Z: utinyint = 12;
|
|
46
|
+
const S: utinyint = 13;
|
|
47
|
+
const E: utinyint = 14;
|
|
48
|
+
const SHIFT_LEFT: utinyint = 15; // also SHIFT LOCK
|
|
49
|
+
// column 2
|
|
50
|
+
const DIGIT_5: utinyint = 16;
|
|
51
|
+
const R: utinyint = 17;
|
|
52
|
+
const D: utinyint = 18;
|
|
53
|
+
const DIGIT_6: utinyint = 19;
|
|
54
|
+
const C: utinyint = 20;
|
|
55
|
+
const F: utinyint = 21;
|
|
56
|
+
const T: utinyint = 22;
|
|
57
|
+
const X: utinyint = 23;
|
|
58
|
+
// column 3
|
|
59
|
+
const DIGIT_7: utinyint = 24;
|
|
60
|
+
const Y: utinyint = 25;
|
|
61
|
+
const G: utinyint = 26;
|
|
62
|
+
const DIGIT_8: utinyint = 27;
|
|
63
|
+
const B: utinyint = 28;
|
|
64
|
+
const H: utinyint = 29;
|
|
65
|
+
const U: utinyint = 30;
|
|
66
|
+
const V: utinyint = 31;
|
|
67
|
+
// column 4
|
|
68
|
+
const DIGIT_9: utinyint = 32;
|
|
69
|
+
const I: utinyint = 33;
|
|
70
|
+
const J: utinyint = 34;
|
|
71
|
+
const DIGIT_0: utinyint = 35;
|
|
72
|
+
const M: utinyint = 36;
|
|
73
|
+
const K: utinyint = 37;
|
|
74
|
+
const O: utinyint = 38;
|
|
75
|
+
const N: utinyint = 39;
|
|
76
|
+
// column 5
|
|
77
|
+
const PLUS: utinyint = 40;
|
|
78
|
+
const P: utinyint = 41;
|
|
79
|
+
const L: utinyint = 42;
|
|
80
|
+
const MINUS: utinyint = 43;
|
|
81
|
+
const PERIOD: utinyint = 44;
|
|
82
|
+
const COLON: utinyint = 45;
|
|
83
|
+
const AT: utinyint = 46;
|
|
84
|
+
const COMMA: utinyint = 47;
|
|
85
|
+
// column 6
|
|
86
|
+
const POUND: utinyint = 48; // £
|
|
87
|
+
const ASTERISK: utinyint = 49;
|
|
88
|
+
const SEMICOLON: utinyint = 50;
|
|
89
|
+
const HOME: utinyint = 51; // CLR/HOME
|
|
90
|
+
const SHIFT_RIGHT: utinyint = 52;
|
|
91
|
+
const EQUALS: utinyint = 53;
|
|
92
|
+
const UP_ARROW: utinyint = 54;
|
|
93
|
+
const SLASH: utinyint = 55;
|
|
94
|
+
// column 7
|
|
95
|
+
const DIGIT_1: utinyint = 56;
|
|
96
|
+
const LEFT_ARROW: utinyint = 57;
|
|
97
|
+
const CONTROL: utinyint = 58;
|
|
98
|
+
const DIGIT_2: utinyint = 59;
|
|
99
|
+
const SPACE: utinyint = 60;
|
|
100
|
+
const COMMODORE: utinyint = 61; // the C= key
|
|
101
|
+
const Q: utinyint = 62;
|
|
102
|
+
const STOP: utinyint = 63; // RUN/STOP
|
|
103
|
+
}
|