@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/mouse.8bs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// @8bitscript/c64/mouse — a 1351 mouse in a control port: is one there,
|
|
2
|
+
// where has it moved, which button is down.
|
|
3
|
+
//
|
|
4
|
+
// Named by "8bitscript".exports["./mouse"] in this package's package.json:
|
|
5
|
+
//
|
|
6
|
+
// import { mouse } from "@8bitscript/c64/mouse";
|
|
7
|
+
//
|
|
8
|
+
// mouse.select(Mouse.PORT_1); // once, at start-up
|
|
9
|
+
// ...
|
|
10
|
+
// waitFrame();
|
|
11
|
+
// mouse.poll(); // once a frame
|
|
12
|
+
// if (mouse.present()) { ... mouse.x() ... }
|
|
13
|
+
//
|
|
14
|
+
// A run-time hardware probe *and* the driver that uses it, because for a
|
|
15
|
+
// mouse the two cannot be separated: a 1351 says nothing about itself
|
|
16
|
+
// except through the same two lines its movement arrives on. A build
|
|
17
|
+
// fitted with one (`--hardware port1=mouse1351`) has `Input.MOUSE` true
|
|
18
|
+
// from @8bitscript/system, meaning this build may use a mouse; this is
|
|
19
|
+
// how it finds out whether one is really plugged in. A program that never
|
|
20
|
+
// imports this file carries none of it.
|
|
21
|
+
//
|
|
22
|
+
// **How a 1351 talks.** It is not a switch like a joystick. Each axis is
|
|
23
|
+
// a counter the mouse advances as it moves, presented on the port's
|
|
24
|
+
// analogue line, which the SID converts and hands back at `$D419` (X) and
|
|
25
|
+
// `$D41A` (Y) — the same registers a paddle uses. VICE's `mouse_1351.c`
|
|
26
|
+
// returns `(counter & 0x7f) + 0x40`, so the value lives in 64..191 and
|
|
27
|
+
// wraps; movement is the *difference* between two readings, not the
|
|
28
|
+
// reading. Sampled once a frame, a difference above half the range is the
|
|
29
|
+
// counter having wrapped the other way, which is what makes it a signed
|
|
30
|
+
// step rather than a jump.
|
|
31
|
+
//
|
|
32
|
+
// **A 1-count is fuzz until it happens twice the same way.** VICE's
|
|
33
|
+
// 1351 is a 7-bit counter — `(x & 0x7f) + 0x40` in `mouse_1351.c` — so a
|
|
34
|
+
// host pixel is often a 1-count step, and at rest the pots flicker 64/65.
|
|
35
|
+
// Applying every 1-count walked the pointer with no host motion. Masking
|
|
36
|
+
// to bits 1–6 and ignoring `|delta| < 3` (the previous driver) threw slow
|
|
37
|
+
// motion away: `last` still advanced, so a slow move never accumulated
|
|
38
|
+
// and the pointer hung until the host jumped several counts in one frame
|
|
39
|
+
// (Studio, 2026-09-07). Holding the first 1-count and applying on the
|
|
40
|
+
// second keeps rest still and lets a slow move through.
|
|
41
|
+
//
|
|
42
|
+
// **How presence is answered, and how honestly.** Measured under VICE
|
|
43
|
+
// with `-controlport1device` at each of its four settings, at rest, both
|
|
44
|
+
// lines read: nothing 255, a joystick 255, paddles 255, a 1351 **64** —
|
|
45
|
+
// exactly the zero of the formula above. So a reading inside the 1351's
|
|
46
|
+
// range is what says a mouse is there, and 255 says one is not. Two
|
|
47
|
+
// things to be honest about: a real paddle sits wherever it was last
|
|
48
|
+
// turned, so a paddle at mid-travel reads in this range too and only
|
|
49
|
+
// movement tells them apart; and a mouse is only *found* once the program
|
|
50
|
+
// has waited a frame after `select()` for the SID's converter to settle.
|
|
51
|
+
// `present()` therefore means "consistent with a 1351", and a program
|
|
52
|
+
// that must be certain should watch for movement before believing it.
|
|
53
|
+
//
|
|
54
|
+
// **The buttons** are not analogue at all: they ride the port's ordinary
|
|
55
|
+
// joystick lines, left on FIRE and right on UP (`mouse_1351.c`), so they
|
|
56
|
+
// are read exactly as a joystick's switches are — which also means a
|
|
57
|
+
// joystick pushed up looks like a right button, and this file will not
|
|
58
|
+
// pretend otherwise.
|
|
59
|
+
//
|
|
60
|
+
// **The port select** is CIA1 port A's top two bits: `%01` reads port 1's
|
|
61
|
+
// analogue lines, `%10` port 2's (confirmed here — `$DC00 = $40` is what
|
|
62
|
+
// made the 1351 readable in port 1). Those same bits are keyboard column
|
|
63
|
+
// outputs, so `select()` leaves the other six high, driving no column;
|
|
64
|
+
// `@8bitscript/c64/keyboard`'s scan writes all eight itself and must be
|
|
65
|
+
// followed by another `select()` before the next `poll()` if both are
|
|
66
|
+
// used. The converter needs about half a millisecond after a select, so
|
|
67
|
+
// the order that works is: `select()` at start-up, then one `poll()` a
|
|
68
|
+
// frame.
|
|
69
|
+
import { paddleX, paddleY, cia1PortA, cia1PortB } from "./index.8bs";
|
|
70
|
+
|
|
71
|
+
export namespace Mouse {
|
|
72
|
+
const PORT_1: utinyint = 0x7F; // %01 in the top bits, no keyboard column driven
|
|
73
|
+
const PORT_2: utinyint = 0xBF; // %10
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
namespace Pot {
|
|
77
|
+
const COUNTER: utinyint = 0x7F; // VICE's 1351 is 7 bits; bit 0 is also fuzz
|
|
78
|
+
const LOWEST: utinyint = 64; // a 1351 at its zero point
|
|
79
|
+
const HIGHEST: utinyint = 191; // and at the other end of its wrap
|
|
80
|
+
const HALF: utinyint = 64; // half the 7-bit wrap space: a bigger step is a wrap
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Per-axis filter for 1-count steps. Rest's 64/65 flicker is +1 then -1
|
|
84
|
+
// and never leaves WAIT; a slow host move is +1, +1 and then tracks at
|
|
85
|
+
// one pixel a count instead of jumping two and stalling on the next.
|
|
86
|
+
namespace Axis {
|
|
87
|
+
const IDLE: utinyint = 0;
|
|
88
|
+
const WAIT_POS: utinyint = 1;
|
|
89
|
+
const RUN_POS: utinyint = 2;
|
|
90
|
+
const WAIT_NEG: utinyint = 3;
|
|
91
|
+
const RUN_NEG: utinyint = 4;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The last counter reading on each axis, and where the pointer has got to.
|
|
95
|
+
let lastX: utinyint = 0;
|
|
96
|
+
let lastY: utinyint = 0;
|
|
97
|
+
let atX: usmallint = 0;
|
|
98
|
+
let atY: usmallint = 0;
|
|
99
|
+
let started: bool = false;
|
|
100
|
+
let hold: array<utinyint, 2>;
|
|
101
|
+
|
|
102
|
+
// Whether the reading the last poll() took was consistent with a 1351.
|
|
103
|
+
// **Kept, rather than asked again**, and that is not an optimisation: the
|
|
104
|
+
// SID's converter runs continuously, so two reads of `$D419` are two
|
|
105
|
+
// different moments, and a program that asks twice in one frame can get
|
|
106
|
+
// two different answers. Measured under x64sc with a 1351 fitted in port
|
|
107
|
+
// 1: `@8bitscript/c64/input`'s poll() concluded *no pointer* while a read
|
|
108
|
+
// taken a few instructions later in the same frame said there was one, so
|
|
109
|
+
// an arrow drawn from one answer and hit-tested from the other flickered
|
|
110
|
+
// out of existence. One reading a frame, and everyone shares it.
|
|
111
|
+
let there: bool = false;
|
|
112
|
+
|
|
113
|
+
// How far the pointer may travel on each axis. A 1351's counters run on
|
|
114
|
+
// for as long as the mouse is pushed, so without a ceiling a pointer
|
|
115
|
+
// shoved off the right of the screen has to be dragged all the way back
|
|
116
|
+
// before it reappears — the accumulator, not the display, is what has to
|
|
117
|
+
// stop. Wide open until a program calls `setLimits()`.
|
|
118
|
+
let limitX: usmallint = 65535;
|
|
119
|
+
let limitY: usmallint = 65535;
|
|
120
|
+
|
|
121
|
+
export namespace mouse {
|
|
122
|
+
// Point the SID's analogue lines at one control port: Mouse.PORT_1 or
|
|
123
|
+
// Mouse.PORT_2. Once, at start-up — the converter needs a frame to
|
|
124
|
+
// settle before the first poll() means anything.
|
|
125
|
+
function select(port: utinyint): void {
|
|
126
|
+
cia1PortA = port;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The ceiling each axis stops at, in mouse counts — `setLimits(319,
|
|
130
|
+
// 199)` for a C64's pixels, `setLimits(39, 24)` for its cells if the
|
|
131
|
+
// program scales before it gets here. Both axes stop at 0 either way.
|
|
132
|
+
// Set it once at start-up; a program that never calls this gets an
|
|
133
|
+
// accumulator that runs as far as the mouse is pushed.
|
|
134
|
+
function setLimits(x: usmallint, y: usmallint): void {
|
|
135
|
+
limitX = x;
|
|
136
|
+
limitY = y;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// One axis' movement since the last reading, added to `at` and held
|
|
140
|
+
// between 0 and `limit`. The difference is taken in all seven bits,
|
|
141
|
+
// and a difference past half the 7-bit wrap space is the counter
|
|
142
|
+
// having gone the other way.
|
|
143
|
+
//
|
|
144
|
+
// **A 1-count is fuzz until it happens twice the same way, then it
|
|
145
|
+
// tracks.** VICE's rest reading flickers 64/65; applying every
|
|
146
|
+
// 1-count walked the pointer with no host motion. Masking to bits 1–6
|
|
147
|
+
// and ignoring `|delta| < 3` *and* updating `last` (the previous
|
|
148
|
+
// driver) ate slow host motion: a trackpad step is often 1–2 counts a
|
|
149
|
+
// frame, so the pointer hung until the host jumped (Studio,
|
|
150
|
+
// 2026-09-07). Confirming the first 1-count, then applying each
|
|
151
|
+
// further 1-count, keeps rest still and lets a slow move through one
|
|
152
|
+
// pixel at a time. A still frame after a *run* drops back to idle so
|
|
153
|
+
// rest cannot walk; a still frame while *waiting* is kept, because a
|
|
154
|
+
// slow host move is often one count then several frames of zero —
|
|
155
|
+
// idling there ate the second count (Studio, 2026-09-07).
|
|
156
|
+
function step(at: usmallint, last: utinyint, now: utinyint, limit: usmallint, axis: utinyint): usmallint {
|
|
157
|
+
let delta: utinyint = (now - last) & Pot.COUNTER;
|
|
158
|
+
let state: utinyint = hold[axis];
|
|
159
|
+
if (delta == 0) {
|
|
160
|
+
if (state == Axis.RUN_POS || state == Axis.RUN_NEG) {
|
|
161
|
+
hold[axis] = Axis.IDLE;
|
|
162
|
+
}
|
|
163
|
+
return at;
|
|
164
|
+
}
|
|
165
|
+
if (delta < Pot.HALF) {
|
|
166
|
+
if (delta == 1) {
|
|
167
|
+
if (state == Axis.WAIT_POS || state == Axis.RUN_POS) {
|
|
168
|
+
hold[axis] = Axis.RUN_POS;
|
|
169
|
+
} else {
|
|
170
|
+
hold[axis] = Axis.WAIT_POS;
|
|
171
|
+
return at;
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
hold[axis] = Axis.RUN_POS;
|
|
175
|
+
}
|
|
176
|
+
if (limit - at < delta) {
|
|
177
|
+
return limit;
|
|
178
|
+
}
|
|
179
|
+
return at + delta;
|
|
180
|
+
}
|
|
181
|
+
let back: utinyint = 128 - delta;
|
|
182
|
+
if (back == 1) {
|
|
183
|
+
if (state == Axis.WAIT_NEG || state == Axis.RUN_NEG) {
|
|
184
|
+
hold[axis] = Axis.RUN_NEG;
|
|
185
|
+
} else {
|
|
186
|
+
hold[axis] = Axis.WAIT_NEG;
|
|
187
|
+
return at;
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
hold[axis] = Axis.RUN_NEG;
|
|
191
|
+
}
|
|
192
|
+
if (back > at) {
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
return at - back;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Read both axes. Once a frame, after waitFrame().
|
|
199
|
+
//
|
|
200
|
+
// **One reading of each line, answering both questions.** Whether a
|
|
201
|
+
// mouse is there and how far it moved come out of the same two bytes,
|
|
202
|
+
// because they are two readings of one moment: the range check says a
|
|
203
|
+
// 1351 and the difference from last frame says how far. Reading the
|
|
204
|
+
// registers a second time later in the frame — which is what
|
|
205
|
+
// `present()` used to do — samples a converter that has moved on.
|
|
206
|
+
function poll(): void {
|
|
207
|
+
let rawX: utinyint = paddleX;
|
|
208
|
+
let rawY: utinyint = paddleY;
|
|
209
|
+
there = rawX >= Pot.LOWEST && rawX <= Pot.HIGHEST && rawY >= Pot.LOWEST && rawY <= Pot.HIGHEST;
|
|
210
|
+
if (started) {
|
|
211
|
+
atX = mouse.step(atX, lastX, rawX, limitX, 0);
|
|
212
|
+
// Y runs opposite the screen. Verified under x64sc (Studio,
|
|
213
|
+
// 2026-09-07): the same sign as X moved the arrow up when the
|
|
214
|
+
// mouse moved down. last and now are swapped so the *delta*
|
|
215
|
+
// flips and the rest position stays 0 (top-left). Reporting
|
|
216
|
+
// `limitY - atY` would park the arrow at the bottom instead.
|
|
217
|
+
atY = mouse.step(atY, rawY, lastY, limitY, 1);
|
|
218
|
+
}
|
|
219
|
+
lastX = rawX;
|
|
220
|
+
lastY = rawY;
|
|
221
|
+
started = true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Is a 1351 in the selected port? True when both lines read inside
|
|
225
|
+
// the 1351's range: nothing, a joystick and paddles at rest all read
|
|
226
|
+
// 255. A paddle turned to the middle of its travel also reads here,
|
|
227
|
+
// so this means "consistent with a mouse" — movement is what proves
|
|
228
|
+
// one.
|
|
229
|
+
//
|
|
230
|
+
// **This is what the last `poll()` saw, not a fresh reading**, so it
|
|
231
|
+
// is false until the first poll and it gives every caller in a frame
|
|
232
|
+
// the same answer. See `there` above for the measurement that made
|
|
233
|
+
// that necessary.
|
|
234
|
+
function present(): bool {
|
|
235
|
+
return there;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Where the pointer has moved to since the program started, in mouse
|
|
239
|
+
// counts. A program scales these to its own screen.
|
|
240
|
+
function x(): usmallint {
|
|
241
|
+
return atX;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function y(): usmallint {
|
|
245
|
+
return atY;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The buttons, on the port's joystick lines: left is FIRE, right is
|
|
249
|
+
// UP. Port 1 is CIA1 port B; port 2 is port A, which is also the
|
|
250
|
+
// register select() writes, so a port-2 mouse's buttons are read from
|
|
251
|
+
// what the port reads back, not from what was written.
|
|
252
|
+
function left(port: utinyint): bool {
|
|
253
|
+
if (port == Mouse.PORT_1) {
|
|
254
|
+
return (cia1PortB & 16) == 0;
|
|
255
|
+
}
|
|
256
|
+
return (cia1PortA & 16) == 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function right(port: utinyint): bool {
|
|
260
|
+
if (port == Mouse.PORT_1) {
|
|
261
|
+
return (cia1PortB & 1) == 0;
|
|
262
|
+
}
|
|
263
|
+
return (cia1PortA & 1) == 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
package/src/pointer.8bs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// @8bitscript/c64/pointer — the C64 behind @8bitscript/pointer: an arrow,
|
|
2
|
+
// drawn with one of the VIC-II's eight sprites.
|
|
3
|
+
//
|
|
4
|
+
// Named by "8bitscript".exports["./pointer"] in this package's package.json,
|
|
5
|
+
// and by "8bitscript".entry.c64 in @8bitscript/pointer's, so a portable
|
|
6
|
+
// program writes
|
|
7
|
+
//
|
|
8
|
+
// import { pointer } from "@8bitscript/pointer";
|
|
9
|
+
//
|
|
10
|
+
// and gets this file on a C64. It is the *visible* half of the pointer
|
|
11
|
+
// @8bitscript/input already reports: `input` says where the pointer is and
|
|
12
|
+
// whether its button went down, and this says what the user sees while
|
|
13
|
+
// they aim it. Two packages rather than one because reading and drawing
|
|
14
|
+
// are separate everywhere else in this library — `text` draws, `input`
|
|
15
|
+
// reads — and because a machine can perfectly well answer `pointerCell()`
|
|
16
|
+
// and have nothing whatever to draw an arrow with. Eight of the nine are
|
|
17
|
+
// in exactly that position today.
|
|
18
|
+
//
|
|
19
|
+
// ---- why a sprite, and which one ------------------------------------------
|
|
20
|
+
//
|
|
21
|
+
// A cursor has to move without disturbing what is under it, and on this
|
|
22
|
+
// machine the only thing that does that is a movable object: a character
|
|
23
|
+
// cell cursor would have to put back the cell it covered, and
|
|
24
|
+
// @8bitscript/text can write a cell but not read one, so it could not.
|
|
25
|
+
// A sprite also moves a pixel at a time where a cell cursor moves eight,
|
|
26
|
+
// which is the difference between aiming at a menu item and guessing at
|
|
27
|
+
// one.
|
|
28
|
+
//
|
|
29
|
+
// **This file owns sprite 0 and the last shape block, and says so.** The
|
|
30
|
+
// VIC has eight sprites in a fixed front-to-back order and sprite 0 is
|
|
31
|
+
// always in front, which is where a cursor belongs — it is over the
|
|
32
|
+
// interface, not in it. The shape goes in the *last* block
|
|
33
|
+
// @8bitscript/c64/sprites owns rather than the first, because a program
|
|
34
|
+
// laying its own shapes out counts up from `sprites.FIRST_BLOCK` and would
|
|
35
|
+
// otherwise land on the cursor's. A program that wants all eight sprites
|
|
36
|
+
// and no cursor simply does not import this file, and pays nothing for it.
|
|
37
|
+
//
|
|
38
|
+
// ---- the position comes from the mouse's accumulator ----------------------
|
|
39
|
+
//
|
|
40
|
+
// `update()` reads ./mouse.8bs's x and y directly, in **pixels**, rather
|
|
41
|
+
// than taking `input.pointerCell()` — a cell is 8 pixels and a cursor that
|
|
42
|
+
// jumped 8 pixels at a time would be the cell cursor this file exists to
|
|
43
|
+
// avoid. Reading the accumulator is free and safe: ./input.8bs's `poll()`
|
|
44
|
+
// is what advances it, once a frame, and this only looks at where it got
|
|
45
|
+
// to. That is the one ordering rule here, and it is the same one `input`
|
|
46
|
+
// already has: **`poll()` then `update()`, once each a frame.** A program
|
|
47
|
+
// that never calls `input.poll()` gets an arrow that never moves.
|
|
48
|
+
//
|
|
49
|
+
// The arrow's tip is its top-left pixel, so the sprite goes at the mouse's
|
|
50
|
+
// pixel plus the screen's origin in the VIC's coordinate space
|
|
51
|
+
// (`sprites.LEFT`, `sprites.TOP` — 24 and 50, documented in
|
|
52
|
+
// ./sprites.8bs): the tip lands exactly on the pixel the mouse is at, and
|
|
53
|
+
// `input.pointerCell()` is the cell that pixel is in, so what the user
|
|
54
|
+
// aims at and what the program hit-tests are the same thing by
|
|
55
|
+
// construction.
|
|
56
|
+
//
|
|
57
|
+
// ---- a build without a mouse pays nothing ---------------------------------
|
|
58
|
+
//
|
|
59
|
+
// `#fact(input.mouse)` is false on a stock C64, so `HAS_MOUSE` is a const,
|
|
60
|
+
// `if (HAS_MOUSE)` is `if (false)`, and the arrow, its 63 bytes of shape
|
|
61
|
+
// and every register write below are neither reached, referenced nor
|
|
62
|
+
// linked. Fitting a mouse is what compiles them in — measured on Studio,
|
|
63
|
+
// where the arrow costs **182 bytes of program and 1 of RAM** on top of
|
|
64
|
+
// the input layer's mouse, and a mouse and its arrow together take that
|
|
65
|
+
// program from 1523 bytes and 28 of RAM to 2208 and 47.
|
|
66
|
+
import { sprites } from "./sprites.8bs";
|
|
67
|
+
import { mouse } from "./mouse.8bs";
|
|
68
|
+
|
|
69
|
+
// Was this build fitted with a mouse? Folded while compiling.
|
|
70
|
+
const HAS_MOUSE: bool = #fact(input.mouse);
|
|
71
|
+
|
|
72
|
+
// The colour the arrow starts in, in the VIC's sixteen. White reads against
|
|
73
|
+
// every background @8bitscript/screen offers, and `setColor` changes it.
|
|
74
|
+
const WHITE: utinyint = 1;
|
|
75
|
+
|
|
76
|
+
// The arrow: 21 rows of 3 bytes, one bit a pixel, left to right — the
|
|
77
|
+
// shape format ./sprites.8bs documents. Only the leftmost byte of each row
|
|
78
|
+
// is set, so the arrow is 8 pixels wide inside a 24-pixel sprite; the tip
|
|
79
|
+
// is the top-left pixel, which is what makes the sprite's position and the
|
|
80
|
+
// pointer's position the same number.
|
|
81
|
+
//
|
|
82
|
+
// X....... XXXXXXXX
|
|
83
|
+
// XX...... XXXXXX..
|
|
84
|
+
// XXX..... XXX.XX..
|
|
85
|
+
// XXXX.... XX...XX.
|
|
86
|
+
// XXXXX... X....XX.
|
|
87
|
+
// XXXXXX.. ......XX
|
|
88
|
+
// XXXXXXX. .......X
|
|
89
|
+
//
|
|
90
|
+
// A const array is data in the program image and never in RAM, so the
|
|
91
|
+
// picture costs no memory the program could have used.
|
|
92
|
+
const ARROW: array<utinyint, 63> = [
|
|
93
|
+
0x80, 0, 0,
|
|
94
|
+
0xC0, 0, 0,
|
|
95
|
+
0xE0, 0, 0,
|
|
96
|
+
0xF0, 0, 0,
|
|
97
|
+
0xF8, 0, 0,
|
|
98
|
+
0xFC, 0, 0,
|
|
99
|
+
0xFE, 0, 0,
|
|
100
|
+
0xFF, 0, 0,
|
|
101
|
+
0xFC, 0, 0,
|
|
102
|
+
0xEC, 0, 0,
|
|
103
|
+
0xC6, 0, 0,
|
|
104
|
+
0x86, 0, 0,
|
|
105
|
+
0x06, 0, 0,
|
|
106
|
+
0x03, 0, 0,
|
|
107
|
+
0, 0, 0,
|
|
108
|
+
0, 0, 0,
|
|
109
|
+
0, 0, 0,
|
|
110
|
+
0, 0, 0,
|
|
111
|
+
0, 0, 0,
|
|
112
|
+
0, 0, 0,
|
|
113
|
+
0, 0, 0,
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
export namespace pointer {
|
|
117
|
+
|
|
118
|
+
// Does this machine draw a pointer at all? A compile-time constant, so
|
|
119
|
+
// a program can lay itself out around the answer — and on the C64 it is
|
|
120
|
+
// "only if this build was fitted with a mouse", which is the same
|
|
121
|
+
// question `input.pointer()` answers at run time.
|
|
122
|
+
const DRAWS: bool = #fact(input.mouse);
|
|
123
|
+
|
|
124
|
+
// The sprite and the shape block this file claims. Named rather than
|
|
125
|
+
// hidden so a program using @8bitscript/c64/sprites alongside a cursor
|
|
126
|
+
// knows which two it may not have.
|
|
127
|
+
const SPRITE: utinyint = 0;
|
|
128
|
+
const BLOCK: utinyint = 254; // sprites.FIRST_BLOCK + sprites.BLOCK_COUNT - 1
|
|
129
|
+
|
|
130
|
+
// Once, at start-up, after input.begin(): copy the arrow into its block
|
|
131
|
+
// and point the sprite at it. The shape is copied rather than pointed
|
|
132
|
+
// at where it lies because the VIC reads its own bank and a const array
|
|
133
|
+
// is wherever the linker put it — the copy loop ./sprites.8bs's header
|
|
134
|
+
// describes. On a build with no mouse this compiles to nothing.
|
|
135
|
+
function begin(): void {
|
|
136
|
+
if (HAS_MOUSE) {
|
|
137
|
+
let i: utinyint = 0;
|
|
138
|
+
while (i < sprites.BYTES) {
|
|
139
|
+
sprites.setShapeByte(pointer.BLOCK, i, ARROW[i]);
|
|
140
|
+
i = i + 1;
|
|
141
|
+
}
|
|
142
|
+
sprites.setShape(pointer.SPRITE, pointer.BLOCK);
|
|
143
|
+
sprites.setColor(pointer.SPRITE, WHITE);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The arrow's colour, in the VIC's sixteen — the same numbers
|
|
148
|
+
// @8bitscript/text's colours are.
|
|
149
|
+
function setColor(color: utinyint): void {
|
|
150
|
+
if (HAS_MOUSE) {
|
|
151
|
+
sprites.setColor(pointer.SPRITE, color);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Once a frame, after input.poll(): put the arrow where the mouse got
|
|
156
|
+
// to, and show it only while a mouse is really answering. `present()`
|
|
157
|
+
// is ./mouse.8bs's probe — a build may be fitted for a mouse and not
|
|
158
|
+
// have one plugged in, and an arrow parked in a corner would be a lie
|
|
159
|
+
// about that.
|
|
160
|
+
function update(): void {
|
|
161
|
+
if (HAS_MOUSE) {
|
|
162
|
+
if (mouse.present()) {
|
|
163
|
+
// The VIC's Y is 8 bits and the screen's 200 rows of pixels
|
|
164
|
+
// start at 50, so the sum is at most 249 and cannot carry
|
|
165
|
+
// out of a byte — which is why this needs no clamp of its
|
|
166
|
+
// own beyond the ceiling input.begin() gave the mouse.
|
|
167
|
+
sprites.place(pointer.SPRITE, sprites.LEFT + mouse.x(), sprites.TOP + mouse.y());
|
|
168
|
+
sprites.show(pointer.SPRITE);
|
|
169
|
+
} else {
|
|
170
|
+
sprites.hide(pointer.SPRITE);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Take the arrow off the screen and leave it off until the next
|
|
176
|
+
// update(). What a program does while it is doing something the user
|
|
177
|
+
// cannot point at.
|
|
178
|
+
function hide(): void {
|
|
179
|
+
if (HAS_MOUSE) {
|
|
180
|
+
sprites.hide(pointer.SPRITE);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
package/src/random.8bs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @8bitscript/c64/random — SID voice 3's hardware entropy.
|
|
2
|
+
//
|
|
3
|
+
// Named by "8bitscript".exports["./random"] in this package's package.json:
|
|
4
|
+
//
|
|
5
|
+
// import { random } from "@8bitscript/c64/random";
|
|
6
|
+
//
|
|
7
|
+
// random.begin();
|
|
8
|
+
// let seed: utinyint = random.byte();
|
|
9
|
+
//
|
|
10
|
+
// Its own import, and not part of ./sid.8bs, on purpose — the same reason
|
|
11
|
+
// @8bitscript/atari8/random stands apart from ./pokey.8bs there. Voice 3's
|
|
12
|
+
// oscillator ($D41B) and envelope generator ($D41C) are readable *hardware*
|
|
13
|
+
// state: with the oscillator set to the noise waveform, $D41B is the top
|
|
14
|
+
// byte of a free-running 23-bit polynomial counter, sampled wherever it
|
|
15
|
+
// happens to be when the CPU reads it, so two runs of the same program
|
|
16
|
+
// never agree. That is exactly what a program wants for a seed and exactly
|
|
17
|
+
// what it does not want anywhere else — a game that reads it inside its
|
|
18
|
+
// logic cannot be replayed, cannot be tested against a recorded input, and
|
|
19
|
+
// cannot be debugged from a screenshot. The root AGENTS.md rule is that
|
|
20
|
+
// hardware entropy is never the deterministic PRNG's seed by default and
|
|
21
|
+
// never arrives without being asked for; this file is the asking.
|
|
22
|
+
//
|
|
23
|
+
// The usual shape: read one byte at start-up, seed a deterministic
|
|
24
|
+
// generator (`@8bitscript/random` or `@8bitscript/random/table`) with it,
|
|
25
|
+
// and never touch this again.
|
|
26
|
+
//
|
|
27
|
+
// ---- this claims voice 3 whole ---------------------------------------------
|
|
28
|
+
//
|
|
29
|
+
// Reading real noise means voice 3 has to actually be making it: `begin()`
|
|
30
|
+
// sets its frequency to the fastest the register allows (the noise LFSR is
|
|
31
|
+
// clocked by the oscillator's own carries, so a low frequency barely
|
|
32
|
+
// changes it between two reads a frame apart) and its waveform to noise,
|
|
33
|
+
// then sets $D418's voice-3-off bit so none of that reaches the speaker.
|
|
34
|
+
// The SID's registers below $D419 are write-only — the chip has no way to
|
|
35
|
+
// report what is already there — so `begin()` writes $D418 as a whole
|
|
36
|
+
// known byte (silent, no filter, volume 0) rather than trying to preserve
|
|
37
|
+
// whatever a program had set before. **Call `begin()` first, before
|
|
38
|
+
// `@8bitscript/c64/sid` touches the volume or the filter**, or its own
|
|
39
|
+
// write will win and this file has no way to know that happened — the two
|
|
40
|
+
// packages do not coordinate, the same way @8bitscript/atari8/random warns
|
|
41
|
+
// about AUDCTL. A program that wants both real sound *and* this seed reads
|
|
42
|
+
// the seed once, at start-up, before it plays a single note; using voice 3
|
|
43
|
+
// for both music and entropy at the same time is not something this file
|
|
44
|
+
// can make safe.
|
|
45
|
+
//
|
|
46
|
+
// ---- what the number actually is -------------------------------------------
|
|
47
|
+
//
|
|
48
|
+
// The noise waveform's LFSR is 23 bits, and $D41B is its top 8, so
|
|
49
|
+
// consecutive reads a few cycles apart are consecutive shifts of one
|
|
50
|
+
// register, not independent samples — the same caveat
|
|
51
|
+
// @8bitscript/atari8/random's header gives POKEY's counter. `word()` is
|
|
52
|
+
// here for a program that wants more than eight bits and knows that;
|
|
53
|
+
// anything that needs many independent values wants a PRNG seeded once
|
|
54
|
+
// from here. Nothing here is a source of cryptographic randomness, on this
|
|
55
|
+
// chip or on POKEY's.
|
|
56
|
+
import { sidRegisters, voice3Oscillator } from "./index.8bs";
|
|
57
|
+
|
|
58
|
+
export namespace random {
|
|
59
|
+
// Claims voice 3: fastest-clocked noise, silenced. See the header for
|
|
60
|
+
// why this must run before anything else touches $D418.
|
|
61
|
+
function begin(): void {
|
|
62
|
+
sidRegisters[14] = 0xFF; // voice 3 frequency, low byte
|
|
63
|
+
sidRegisters[15] = 0xFF; // voice 3 frequency, high byte — fastest LFSR clock
|
|
64
|
+
sidRegisters[18] = 0x80; // voice 3 control: noise waveform, gate off
|
|
65
|
+
sidRegisters[24] = 0x80; // $D418: voice 3 off, filter off, volume 0 — a whole known byte, not a read-modify-write of a write-only register
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// One byte from the oscillator, 0-255.
|
|
69
|
+
function byte(): utinyint {
|
|
70
|
+
return voice3Oscillator;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Two bytes as one 16-bit value. See the header: the two reads are
|
|
74
|
+
// consecutive shifts of one register, close together in time, so this
|
|
75
|
+
// is wider than `byte()` but not twice as random.
|
|
76
|
+
function word(): usmallint {
|
|
77
|
+
let high: usmallint = voice3Oscillator;
|
|
78
|
+
let low: usmallint = voice3Oscillator;
|
|
79
|
+
return high * 256 + low;
|
|
80
|
+
}
|
|
81
|
+
}
|