@moonbase-machines/c64 0.0.0-stage → 0.5.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/rom.js ADDED
@@ -0,0 +1,537 @@
1
+ // The Commodore 64's ROMs in JavaScript, since the ROMs are Commodore's own: the original's
2
+ // counterpart of @moonbase-machines/apple2e/rom.js for the Apple. The games run as they are, their own
3
+ // bytes on the 6510; where one calls into the KERNAL or BASIC, the routine is answered here, at
4
+ // its address, as the remake's KERNAL answers it (kernal.js), and the machine goes on as the
5
+ // ROM's would have left it. A routine not here stops the machine and says where, as the remake
6
+ // does.
7
+ //
8
+ // const roms = standIn() the three ROMs' places, for new C64(game, roms)
9
+ // install(machine) the routines, on the machine's processor
10
+ //
11
+ // What the ROM images hold is only what a game reads as data, each byte with its reason: the
12
+ // processor's three vectors, the KERNAL's keyboard tables (kernal.js `kernalByte`), and three
13
+ // bytes Zoopak's code reads with BIT only to skip the two bytes after it (BIT $AE91, $FBB1 and
14
+ // $FFA9), whose top two bits the flags take. Any other byte read stops the machine too. The
15
+ // character set is moonbase's own 5 by 7 font (@moonbase-machines/charset/font.js), the Apple's: a game
16
+ // that draws in its own characters (Zoopak's, from RAM) is not touched by it.
17
+ //
18
+ // The titles it runs, each held to the remake and to Commodore's ROMs, are `STANDS_IN`: Zoopak,
19
+ // and Goldfields, whose Blitz! code calls BASIC's ROM as its library (basic-rom.js); and Crayon
20
+ // on the 64, Moonbase's own, held to Commodore's ROMs.
21
+ import { install as kernal, kernalByte, VIC, VECTORS } from './kernal.js';
22
+ import { CONSTANTS } from './float.js';
23
+ import { chargen } from '@moonbase-machines/charset/chargen.js';
24
+ import { install as basicRom, printError, evaluateInMemory } from './basic-rom.js';
25
+ import { lines, direct, crunch, store, list, program as programLines, dataItems } from './basic-lang.js';
26
+ import { instantiate, evaluator } from './basic-compile.js';
27
+ import { Basic } from './basic.js';
28
+ import { BasicError, ERRORS, val, scanNumber } from './numbers.js';
29
+
30
+ export const STANDS_IN = ['zoopak', 'goldfields', 'crayon64', 'crayon64-studio'];
31
+
32
+ const BYTES = {
33
+ 0xfffa: 0x43, 0xfffb: 0xfe, // NMI: $FE43
34
+ 0xfffc: 0xe2, 0xfffd: 0xfc, // RESET: $FCE2, the cold start
35
+ 0xfffe: 0x48, 0xffff: 0xff, // IRQ and BRK: $FF48
36
+ 0xae91: 0xbc, 0xfbb1: 0x8d, 0xffa9: 0xdd, // read by BIT only: N set, and V for $FFA9
37
+ };
38
+
39
+ // a ROM's 8K and which of its bytes are here; reading one that is not stops the machine
40
+ function image(from) {
41
+ const bytes = new Uint8Array(0x2000), known = new Uint8Array(0x2000);
42
+ for (let a = from; a < from + 0x2000; a++) {
43
+ let v = BYTES[a];
44
+ if (v === undefined && from === 0xe000) try { v = kernalByte(a); } catch { v = undefined; }
45
+ // (BASIC's constants, which its arithmetic reads from the ROM: float.js)
46
+ for (const [at, bytes] of Object.entries(CONSTANTS)) if (a >= +at && a < +at + bytes.length) v = bytes[a - at];
47
+ if (v !== undefined) { bytes[a - from] = v; known[a - from] = 1; }
48
+ }
49
+ return Object.assign(bytes, { known, from });
50
+ }
51
+ export function standIn() {
52
+ return { kernal: image(0xe000), basic: image(0xa000), chargen: chargen(), standIn: true };
53
+ }
54
+
55
+ // The keyboard's matrix as the KERNAL's scan finds it: the first key held that is not a shift
56
+ // key, as its code (column * 8 + row), and the shift keys down as $028D has them (1 SHIFT, 2 the
57
+ // Commodore key, 4 CTRL)
58
+ const SHIFTS = { 0x0f: 1, 0x34: 1, 0x3d: 2, 0x3a: 4 };
59
+ function held(m) {
60
+ let code = null, shift = 0;
61
+ for (let column = 0; column < 8; column++) for (let row = 0; row < 8; row++) {
62
+ if (!(m.keys[column] & 1 << row)) continue;
63
+ const k = column * 8 + row;
64
+ if (SHIFTS[k]) shift |= SHIFTS[k]; else if (code === null) code = k;
65
+ }
66
+ return code === null && !shift ? null : { code, shift }; // (code null: only shift keys down)
67
+ }
68
+
69
+ export function install(m) {
70
+ const c = m.cpu, r = a => m.read(a), w = (a, v) => m.write(a, v & 0xff);
71
+ // (CHRGET and CHRGOT, $0073 and $0079, BASIC's in RAM, answered as its ROM's are, whatever is banked)
72
+ const banked = pc => (pc >= 0xe000 && m.port & 2) || (pc >= 0xa000 && pc < 0xc000 && (m.port & 3) === 3) || pc === 0x73 || pc === 0x79;
73
+ const vector = a => r(a) | r(a + 1) << 8;
74
+ // a routine the game's own code calls, run to its return: the keyboard decode a game has
75
+ // pointed $028F at (Tell-A-Tale's)
76
+ const call = at => {
77
+ const back = c.pc, sp = c.sp;
78
+ c.push((back - 1) >> 8); c.push((back - 1) & 0xff); c.pc = at;
79
+ for (let guard = 0; guard < 1e6 && !(c.pc === back && c.sp === sp); guard++) c.step();
80
+ };
81
+ const before = new Set(c.hooks.keys());
82
+ kernal({ cpu: c, read: r, write: w, drive: m.bus, call, held: () => held(m), log: m.log });
83
+ // and what the remake's KERNAL leaves to the remake: GETIN, the next key in the keyboard's
84
+ // buffer ($0277, $C6 of them), or 0, or from the drive when the input is a file on it, as CHRIN
85
+ // reads it; SCNKEY
86
+ // ($FF9F, and $EA87 itself), the interrupt's keyboard read; SCREEN and IOBASE, the screen's size and the I/O's place
87
+ c.hooks.set(0xffe4, () => {
88
+ if (r(0x99) >= 4) return c.hooks.get(0xf157)(c); // (from a file, CHRIN's own way: $F14A on to $F166)
89
+ const n = r(0xc6);
90
+ if (!n) { c.a = 0; c.nz(0); c.c = 0; return; }
91
+ c.a = r(0x277); for (let i = 0; i < n - 1; i++) w(0x277 + i, r(0x278 + i)); w(0xc6, n - 1); c.nz(c.a); c.c = 0;
92
+ });
93
+ c.hooks.set(0xff9f, c.hooks.get(0xea7b));
94
+ c.hooks.set(0xea87, c.hooks.get(0xea7b)); // (and SCNKEY at its own address, past the jump table)
95
+ c.hooks.set(0xffed, () => { c.x = 40; c.y = 25; });
96
+ c.hooks.set(0xfff3, () => { c.x = 0x00; c.y = 0xdc; });
97
+ // The jump table's routines that go through a vector in RAM ($031A to $0333), as the KERNAL's
98
+ // own do (CHROUT, $FFD2, is JMP ($0326)): the routine at the vector's own place, and the table's
99
+ // entry on through the vector, to it or to a game's own routine there (Zoopak's MENU points
100
+ // CHROUT at its own, which draws on its screen and ends JMP $F1CA). But LOAD and SAVE go to the
101
+ // machine's own, at $F49E and $F5DD (machine.js), before their vectors, as with
102
+ // Commodore's ROM: the drive is answered at the KERNAL's routines, not on the serial bus's
103
+ // wires, so a game's own fast loader at the vector (Zoopak's, at $C20C) is gone past.
104
+ const VECTORED = { 0xffc0: [0x31a, 0xf34a], 0xffc3: [0x31c, 0xf291], 0xffc6: [0x31e, 0xf20e], 0xffc9: [0x320, 0xf250], 0xffcc: [0x322, 0xf333],
105
+ 0xffcf: [0x324, 0xf157], 0xffd2: [0x326, 0xf1ca], 0xffe1: [0x328, 0xf6ed], 0xffe4: [0x32a, 0xf13e], 0xffe7: [0x32c, 0xf32f] };
106
+ for (const [entry, [via, own]] of Object.entries(VECTORED).map(([e, v]) => [+e, v])) {
107
+ c.hooks.set(own, c.hooks.get(entry));
108
+ c.hooks.set(entry, () => { c.pc = vector(via); return false; });
109
+ }
110
+ c.hooks.set(0xffd5, () => { w(0xc3, c.x); w(0xc4, c.y); c.pc = 0xf49e; return false; });
111
+ c.hooks.set(0xffd8, () => { c.pc = 0xf5dd; return false; });
112
+ // BASIC's ROM: the routines a program's machine code calls (basic-rom.js), an
113
+ // expression's value from the program being run (its compiled parameters)
114
+ basicRom({ cpu: c, read: r, write: w, chrout: c.hooks.get(0xf1ca),
115
+ // (the program RUN has compiled, the boot, its own compiled parameters; any other text, a
116
+ // program BASIC is not running (Blitz!'s GAME), read from memory and its variables there)
117
+ evaluate: at => {
118
+ const p = program?.params?.[at];
119
+ if (!p) return inMemory(at);
120
+ const [end, f, place] = p;
121
+ return { value: f(basic), end, place };
122
+ } });
123
+ const inMemory = evaluateInMemory({ cpu: c, read: r, write: w, ram: m.ram });
124
+
125
+ const ours = [...c.hooks.keys()].filter(a => !before.has(a));
126
+ // the return from an interrupt, as the KERNAL's ends ($EA81): Y, X and A back, and RTI
127
+ const exit = () => { c.y = c.pull(); c.x = c.pull(); c.a = c.pull(); c.p = c.pull(); c.pc = c.pull() | c.pull() << 8; };
128
+ const set = (a, f) => c.hooks.set(a, f);
129
+ // CINT ($FF5B), as the KERNAL's goes on after the screen: it waits for the raster's line 0
130
+ // ($D012), takes PAL or NTSC from the VIC's raster interrupt flag ($D019, kept at $02A6), sets
131
+ // CIA 1's timer A for it ($4025 or $4295 cycles, a sixtieth of a second) and starts it, with its
132
+ // interrupt on, and lets the serial bus's clock line go. The VIC is read as the KERNAL reads
133
+ // it, so that a machine that counts its reads counts the same.
134
+ { const screen = c.hooks.get(0xff5b);
135
+ c.hooks.set(0xff5b, () => {
136
+ screen(c);
137
+ for (let guard = 0; guard < 1e5 && r(0xd012) !== 0; guard++);
138
+ w(0x2a6, r(0xd019) & 1);
139
+ const timer = r(0x2a6) ? 0x4025 : 0x4295;
140
+ w(0xdc04, timer); w(0xdc05, timer >> 8);
141
+ w(0xdc0d, 0x81); w(0xdc0e, (r(0xdc0e) & 0x80) | 0x11);
142
+ // and the serial bus's clock line let go ($EE8E): CIA 2's port read back, its bit 4 set
143
+ w(0xdd00, r(0xdd00) | 0x10);
144
+ }); }
145
+ // The interrupt's own routine and its ends, which a game's routine jumps on to: the work (the
146
+ // remake's), the CIA's interrupt answered ($DC0D read), and the return
147
+ for (const a of [0xea31, 0xea34, 0xea7b, 0xea7e]) { const f = c.hooks.get(a); set(a, () => { f(c); r(0xdc0d); exit(); return false; }); }
148
+ set(0xea81, () => { exit(); return false; });
149
+ // the way in: A, X and Y kept, and on through $0316 for BRK or $0314 for an interrupt ($FF48)
150
+ set(0xff48, () => { c.push(c.a); c.push(c.x); c.push(c.y); c.pc = vector(r(0x104 + c.sp) & 0x10 ? 0x316 : 0x314); return false; });
151
+ // NMI, the RESTORE key: on through $0318 ($FE43), whose own routine ($FE47) answers CIA 2 and returns
152
+ set(0xfe43, () => { c.i = 1; c.pc = vector(0x318); return false; });
153
+ set(0xfe47, () => { r(0xdd0d); c.p = c.pull(); c.pc = c.pull() | c.pull() << 8; return false; });
154
+ // the cold start ($FCE2), and BASIC ready for a command (`ready`, at $A480)
155
+ set(0xfce2, () => { coldStart(); c.i = 0; c.pc = 0xa480; return false; });
156
+ set(0xa480, () => ready());
157
+ // where BASIC's LOAD comes back to, when the file loaded has not taken the stack: READY again
158
+ set(0xe178, () => {
159
+ // (a program, loaded at BASIC's start, has its variables after it, as a LOAD from the keyboard sets them)
160
+ if (!r(0xb9)) { w(0x2d, r(0xae)); w(0x2e, r(0xaf)); }
161
+ print('\rREADY.\r'); c.sp = 0xff; c.pc = 0xa480; return false;
162
+ });
163
+ // RUN ($A871) and BASIC going on to its next statement ($A7AE), where SYS's machine code and
164
+ // GET's GETIN come back to
165
+ set(0xa871, () => { run(); c.pc = 0xa7ae; return false; });
166
+ set(0xa7ae, () => go());
167
+ // CLR as RUN's own start calls it ($A659: the text pointer at the program's start, and CLEARC,
168
+ // the variables and arrays after its end ($2D), the strings down from the top, $37) and LINKPRG
169
+ // ($A533: each line's link set to the next, from TXTTAB, to the line whose link's high byte is
170
+ // 0), as a cruncher calls them at its end, over the program it has unpacked, before $A7AE runs
171
+ // it: the program read afresh each time, so that $A7AE starts it at its first line
172
+ set(0xa659, () => {
173
+ w(0x2f, r(0x2d)); w(0x30, r(0x2e)); w(0x31, r(0x2d)); w(0x32, r(0x2e)); w(0x33, r(0x37)); w(0x34, r(0x38));
174
+ run(); cont = null; return true;
175
+ });
176
+ set(0xa533, () => {
177
+ for (let at = r(0x2b) | r(0x2c) << 8, k = 0; r(at + 1) && k < 0x10000; k++) {
178
+ let end = at + 4; while (r(end)) end++;
179
+ w(at, (end + 1) & 0xff); w(at + 1, (end + 1) >> 8); at = end + 1;
180
+ }
181
+ if (basic) compile();
182
+ return true;
183
+ });
184
+ // BASIC's errors, as its ROM has them: X the error's number, $A437 on through the vector at
185
+ // $0300 (to $E38B, unless a program has moved it), there to $A43A for an error (X under $80) or
186
+ // READY ($A474) for none; and $A43A the message, IN the line (CURLIN, $39), and READY
187
+ set(0xa437, () => { c.pc = r(0x300) | r(0x301) << 8; return false; });
188
+ set(0xe38b, () => { c.pc = c.x & 0x80 ? 0xa474 : 0xa43a; return false; });
189
+ set(0xa43a, () => { printError(fromRom, '?' + ERRORS[c.x - 1] + ' ERROR', r(0x39) | r(0x3a) << 8); c.sp = 0xff; c.pc = 0xa480; return false; });
190
+ // (the KERNAL's routines, CLRCHN and CHROUT, called as the ROM's error routine calls them: a JSR
191
+ // from it, at $A447, through the jump table and the vector a program may have moved, run on the
192
+ // processor until it returns there)
193
+ const jsr = (at, from) => {
194
+ const sp = c.sp, ret = from + 2, pc = c.pc;
195
+ c.push(ret >> 8); c.push(ret & 0xff); c.pc = at;
196
+ for (let k = 0; k < 1e6 && !(c.pc === ret + 1 && c.sp === sp); k++) c.step();
197
+ c.pc = pc;
198
+ };
199
+ const fromRom = { cpu: { get a() { return c.a; }, set a(v) { c.a = v; }, hooks: { get: at => () => jsr(at, 0xa447) } }, write: w };
200
+ set(0xa474, () => { print('\rREADY.\r'); c.sp = 0xff; c.pc = 0xa480; return false; });
201
+ // each of these only while its ROM is in; under it, the RAM's own code runs
202
+ // (and a routine that returns from its caller too, `'return'`, has its caller's return address
203
+ // pulled before its own RTS: basic-rom.js's $E206). An error in a routine the machine code has
204
+ // called goes on as the ROM's does, above; in one a statement of the program has called
205
+ // (`inStatement`, go() below), to the statement's own.
206
+ for (const a of [...ours, 0xa437, 0xe38b, 0xa43a, 0xa474, 0xea81, 0xff48, 0xfe43, 0xfe47, 0xfce2, 0xa480, 0xe178, 0xa871, 0xa7ae, 0xa659, 0xa533]) {
207
+ const f = c.hooks.get(a);
208
+ c.hooks.set(a, cpu => {
209
+ if (!banked(cpu.pc)) return false;
210
+ let result;
211
+ try { result = f(cpu); }
212
+ catch (err) {
213
+ const word = err instanceof BasicError && !inStatement && /^\?(.*) ERROR$/.exec(err.message)?.[1];
214
+ if (!ERRORS.includes(word)) throw err;
215
+ cpu.x = ERRORS.indexOf(word) + 1; cpu.pc = 0xa437; return false;
216
+ }
217
+ if (result === 'return') { cpu.pull(); cpu.pull(); return true; }
218
+ return result;
219
+ });
220
+ }
221
+
222
+ const chrout = ch => { const { a, x, y } = c; c.a = ch; c.hooks.get(0xf1ca)(c); Object.assign(c, { a, x, y }); };
223
+ const print = s => { for (const ch of s) chrout(ch.charCodeAt(0)); };
224
+
225
+ // The machine as the KERNAL and BASIC start it: the I/O (the CIAs, CIA 1's timer A the
226
+ // interrupt sixty times a second, the VIC through CINT), the RAM's first pages cleared and
227
+ // the KERNAL's own variables and vectors set, and the screen cleared with the light blue cursor
228
+ function coldStart() {
229
+ c.i = 1; c.sp = 0xff; m.port = 0x37; m.portDir = 0x2f;
230
+ for (let a = 2; a < 0x400; a++) if (a < 0x100 || a >= 0x200) m.ram[a] = 0;
231
+ w(0xdc0d, 0x7f); w(0xdd0d, 0x7f); w(0xdc00, 0x7f); w(0xdc02, 0xff); w(0xdc03, 0x00);
232
+ w(0xdc0e, 0x08); w(0xdc0f, 0x08); w(0xdd0e, 0x08); w(0xdd0f, 0x08); // the timers stopped, one-shot
233
+ w(0xdd00, 0x07); w(0xdd02, 0x3f);
234
+ w(0xdc04, 0x25); w(0xdc05, 0x40); w(0xdc0d, 0x81); w(0xdc0e, 0x11); // timer A: $4025 cycles, its interrupt on, started
235
+ w(0x282, 0x08); w(0x284, 0xa0); w(0x283, 0x00); w(0x281, 0x00); // the memory BASIC has, $0800 to $A000
236
+ w(0x288, 0x04); w(0x286, 0x0e); w(0x289, 0x0a); w(0x28b, 0x04); w(0x28c, 0x0a); w(0x28a, 0x00);
237
+ w(0x28f, 0x48); w(0x290, 0xeb); // the keyboard's decode, the KERNAL's own ($EB48)
238
+ w(0xf5, 0x81); w(0xf6, 0xeb); // its table, the unshifted ($EB81)
239
+ w(0x2a6, 0x01); // PAL
240
+ w(0x9a, 0x03); w(0x99, 0x00); w(0x9d, 0x80); w(0xcc, 0x01); w(0xcd, 0x14);
241
+ // BASIC's pointers, as it starts with no program (NEW): its text at $0801, the variables and
242
+ // arrays after its empty end, the strings down from the top of its memory, $A000
243
+ const word = (at, v) => { w(at, v); w(at + 1, v >> 8); };
244
+ word(0x2b, 0x0801); word(0x2d, 0x0803); word(0x2f, 0x0803); word(0x31, 0x0803); word(0x33, 0xa000); word(0x37, 0xa000);
245
+ // and its two jumps in RAM: its functions' (JMPER, $54, which a program sets the address of,
246
+ // Blitz!'s calls to the floating point), and USR's ($0310, to ILLEGAL QUANTITY until it is set)
247
+ w(0x54, 0x4c); w(0x310, 0x4c); word(0x311, 0xb248);
248
+ // the rest of BASIC's start ($E453, $E3BF): its vectors (the error's, the main loop's, the
249
+ // tokeniser's, the lister's, a statement's and an expression's routines), the conversions'
250
+ // ($03 and $05: a number to a whole one, and back), CHRGET and CHRGOT in RAM (INC $7A; BNE;
251
+ // INC $7B; LDA $EA60; CMP #$3A; BCS; CMP #$20; BEQ $73; SEC; SBC #$30; SEC; SBC #$D0; RTS, and
252
+ // RND's first seed after them), the garbage collector's step, the fill byte, the input device,
253
+ // the temporary strings' stack empty ($16 at $19), and two bytes of the stack
254
+ [0xe38b, 0xa483, 0xa57c, 0xa71a, 0xa7e4, 0xae86].forEach((v, i) => word(0x300 + 2 * i, v));
255
+ word(0x03, 0xb1aa); word(0x05, 0xb391);
256
+ [0xe6, 0x7a, 0xd0, 0x02, 0xe6, 0x7b, 0xad, 0x60, 0xea, 0xc9, 0x3a, 0xb0, 0x0a, 0xc9, 0x20, 0xf0, 0xef, 0x38, 0xe9, 0x30, 0x38, 0xe9, 0xd0, 0x60,
257
+ 0x80, 0x4f, 0xc7, 0x52, 0x58].forEach((v, i) => w(0x73 + i, v));
258
+ w(0x53, 0x03); w(0x68, 0); w(0x13, 0); w(0x18, 0); w(0x1fd, 1); w(0x1fc, 1); w(0x16, 0x19);
259
+ m.ram[0x800] = 0; m.ram[0x801] = 0; m.ram[0x802] = 0;
260
+ VECTORS.forEach((v, i) => { w(0x314 + 2 * i, v); w(0x315 + 2 * i, v >> 8); });
261
+ VIC.forEach((v, i) => w(0xd000 + i, v));
262
+ c.hooks.get(0xff5b)(c); // CINT: the VIC and the screen
263
+ print('\rREADY.\r');
264
+ }
265
+
266
+ // BASIC's main loop ($A480): a line typed read into the buffer at $0200, as INLIN reads it
267
+ // ($A560: CHRIN's characters, the screen editor's line, to RETURN; past 88, STRING TOO LONG;
268
+ // then a RETURN printed), and carried out. An empty line is read again. A line with a number
269
+ // goes into the program in its place (its number as LINGET reads it, the rest tokenized as
270
+ // CRUNCH tokenizes it: basic-lang.js), the variables cleared, and the next line read.
271
+ // Any other runs at once (direct mode, `direct` below), and READY. Two keep their ways:
272
+ // LOAD"name",device[,1], with the KERNAL's own messages, the file loaded
273
+ // as the KERNAL's LOAD loads it, and back as BASIC's LOAD comes back, through the stack, which a
274
+ // file loaded over it (Zoopak's) takes to its own start; and RUN, by $A871.
275
+ let typed = [];
276
+ function ready() {
277
+ for (;;) {
278
+ if (c.hooks.get(0xf157)(c) === 'wait') return 'wait'; // (CHRIN's own routine, past its jump-table entry and vector)
279
+ if (c.a === 0x0d) break;
280
+ typed.push(c.a);
281
+ if (typed.length >= 0x59) { typed = []; c.x = 0x17; c.pc = 0xa437; return false; }
282
+ }
283
+ const text = typed; typed = [];
284
+ text.forEach((ch, i) => w(0x200 + i, ch)); w(0x200 + text.length, 0);
285
+ chrout(0x0d);
286
+ let i = 0;
287
+ while (text[i] === 0x20) i++;
288
+ if (i === text.length) return 'wait';
289
+ w(0x3a, 0xff);
290
+ // a line for the program: its number ($A96B, LINGET: digits, spaces passed over, past 63999
291
+ // a SYNTAX ERROR), its text tokenized, put in its place, and the variables cleared
292
+ if (text[i] >= 0x30 && text[i] <= 0x39) {
293
+ let n = 0;
294
+ for (; i < text.length && (text[i] === 0x20 || text[i] >= 0x30 && text[i] <= 0x39); i++) {
295
+ if (text[i] === 0x20) continue;
296
+ if (n >= 6400) { c.x = 0x0b; c.pc = 0xa437; return false; }
297
+ n = n * 10 + text[i] - 0x30;
298
+ }
299
+ const end = store(m.ram, r(0x2b) | r(0x2c) << 8, n, crunch(text.slice(i)));
300
+ for (const at of [0x2d, 0x2f, 0x31]) { w(at, end); w(at + 1, end >> 8); }
301
+ w(0x33, r(0x37)); w(0x34, r(0x38));
302
+ basic?.clr(); cont = null;
303
+ return 'wait';
304
+ }
305
+ const command = String.fromCharCode(...text.slice(i)).trim();
306
+ if (command === 'RUN') { c.pc = 0xa871; return false; }
307
+ const load = /^LOAD"([^"]*)"(?:,(\d+))?(?:,(\d+))?$/.exec(command);
308
+ if (load) {
309
+ const [, name, device = '1', secondary = '0'] = load;
310
+ for (let i = 0; i < name.length; i++) w(0x200 + i, name.charCodeAt(i));
311
+ w(0xb7, name.length); w(0xbb, 0x00); w(0xbc, 0x02); w(0xba, +device); w(0xb9, +secondary); w(0xb8, 1);
312
+ // (the KERNAL's messages, each its return first: \rSEARCHING FOR name, \rLOADING)
313
+ print('\rSEARCHING FOR ' + name);
314
+ c.a = 0; c.x = 0x01; c.y = 0x08; // (where a file with secondary address 0 goes: $0801)
315
+ // the stack as BASIC's LOAD has it, its return to BASIC ($E178) at $01F8, then the load
316
+ c.sp = 0xf7; w(0x1f8, 0x77); w(0x1f9, 0xe1);
317
+ m.load();
318
+ if (c.c) { print('\r?FILE NOT FOUND ERROR'); c.sp = 0xff; print('\rREADY.\r'); return 'wait'; }
319
+ // (LOADING once the file is found; the registers LOAD leaves, its end, kept for BASIC)
320
+ const { a, x, y } = c; print('\rLOADING'); Object.assign(c, { a, x, y, c: 0 });
321
+ return true; // as the KERNAL's LOAD returns: RTS, to wherever $01F8 now says
322
+ }
323
+ // direct mode: the line tokenized in the buffer, as CRUNCH leaves it, and run after the
324
+ // program's statements
325
+ crunch(text.slice(i)).forEach((b, k) => w(0x200 + k, b));
326
+ return runDirect();
327
+ }
328
+
329
+ // A program run (RUN): the BASIC program in memory, at TXTTAB ($2B), read (basic-lang.js)
330
+ // and compiled (basic-compile.js) as the remake's are, and its statements carried out
331
+ // by the remake's BASIC (basic.js), on `engine`, this machine as that BASIC asks for one:
332
+ // its memory, the registers it calls the KERNAL with, and the KERNAL's routines by name,
333
+ // answered here. GET and SYS go through the processor, as BASIC's own do: GET calls GETIN at
334
+ // $FFE4, SYS jumps to the machine code; both come back to $A7AE,
335
+ // and the program goes on. LOAD from a program loads, and runs the program again from its start,
336
+ // its variables kept, or the program it has loaded (GAME, over the boot). An error is BASIC's
337
+ // message (basic-rom.js) and READY.
338
+ let program = null, basic = null, statements = [], getting = null, inStatement = false;
339
+ // each step's line, for CURLIN ($39) as it runs and an error's IN (direct mode's $FFFF: none),
340
+ // and where CONT goes on, the step after a STOP or END, until the program is changed
341
+ let lineOf = [], cont = null, waiting = null, paced = -1, budget = 0;
342
+ const DIRECT = 0x10000; // the label the direct statements start at
343
+ const YIELD = Symbol('the processor goes on');
344
+ const routine = at => e => { Object.assign(c, { a: e.a, x: e.x, y: e.y }); c.hooks.get(at)(c); Object.assign(e, { a: c.a, x: c.x, y: c.y, c: c.c, z: c.z, n: c.n, v: c.v }); };
345
+ const engine = {
346
+ get mem() { return m.ram; }, read: r, write: w,
347
+ a: 0, x: 0, y: 0, c: 0, z: 0, n: 0, v: 0, pc: 0, ended: false, idle: 0, polled: false, inside: 0, keys: [], delay: 0, cycles: 0,
348
+ title: { seed: 0x11 },
349
+ routines: {
350
+ chrout: routine(0xf1ca), chrin: routine(0xf157), getin: routine(0xf13e), clrchn: routine(0xf333), chkin: routine(0xf20e), chkout: routine(0xf250),
351
+ open: routine(0xf34a), close: routine(0xf291), setnam: routine(0xffbd), settim: routine(0xffdb), setlfs: routine(0xffba),
352
+ load: e => { Object.assign(c, { a: e.a, x: e.x, y: e.y }); m.load(); Object.assign(e, { a: c.a, x: c.x, y: c.y, c: c.c }); },
353
+ // SYS: BASIC's text pointer at the text after it, its address at $14 as GETADR leaves it,
354
+ // and on through SYS's own ($E130, basic-rom.js), which keeps the registers, back to $A7AE
355
+ sys: (e, at, text) => {
356
+ w(0x7a, text); w(0x7b, text >> 8); w(0x14, at); w(0x15, at >> 8);
357
+ c.push((0xa7ae - 1) >> 8); c.push((0xa7ae - 1) & 0xff); c.pc = 0xe130;
358
+ return YIELD;
359
+ },
360
+ // LOAD in a program
361
+ 'basic-load': (e, [name, device = 1, sa = 0]) => {
362
+ for (let k = 0; k < name.length; k++) w(0x200 + k, name.charCodeAt(k));
363
+ w(0xb7, name.length); w(0xbb, 0x00); w(0xbc, 0x02); w(0xba, device); w(0xb9, sa); w(0xb8, 0);
364
+ Object.assign(e, { a: 0, x: 0x01, y: 0x08 });
365
+ e.routines.load(e);
366
+ if (e.c) throw new BasicError('?FILE NOT FOUND ERROR');
367
+ if (!sa) compile(); else e.pc = 0;
368
+ },
369
+ },
370
+ };
371
+ function compile(all = lines(m.ram, r(0x2b) | r(0x2c) << 8)) {
372
+ program = instantiate(all, { name: 'program', source: 'BASIC\'s text in memory', mem: m.ram, cost: 0, render: () => '' });
373
+ lineOf = []; let line = 0xffff, start = 0;
374
+ for (const s of all) { if (s.label !== undefined) { line = s.line ?? 0xffff; if (s.label === DIRECT) start = lineOf.length; } else lineOf.push(line); }
375
+ statements = all.filter(s => s.label === undefined);
376
+ if (basic) basic.items = dataItems(m.ram, r(0x2b) | r(0x2c) << 8); // (DATA, as READ reads it)
377
+ engine.pc = 0; engine.ended = false;
378
+ return start;
379
+ }
380
+ function run() { basic = new Basic(engine, { quiet: true }); engine.basic = basic; compile(); }
381
+ // INPUT from the keyboard (Microsoft's INPUT, QINLIN and INLOOP): not in direct mode; the
382
+ // prompt and "? ", a line read as the main loop reads one (CHRIN, the screen editor's), and
383
+ // its items taken in turn: a string quoted to its closing quote, or to a comma or a colon; a
384
+ // number as FIN reads it, which a comma, a colon or the line's end must follow, or
385
+ // "?REDO FROM START" and the INPUT again; a line run out before the variables, "?? " and
386
+ // another; items left, "?EXTRA IGNORED"; an empty line, the program ended, as END ends it.
387
+ // While a line is read, the run waits here (`inputting`).
388
+ let inputting = null;
389
+ function input(st, k) {
390
+ if (lineOf[k] === 0xffff) throw new BasicError('?ILLEGAL DIRECT ERROR');
391
+ if (!inputting) {
392
+ inputting = { k, n: 0, text: null, at: 0, typed: [] };
393
+ if (st.prompt !== null) for (const ch of st.prompt) chrout(ch.charCodeAt(0));
394
+ print('? ');
395
+ }
396
+ const s = inputting;
397
+ for (;;) {
398
+ if (s.text === null) {
399
+ // a line, to RETURN
400
+ for (;;) {
401
+ if (c.hooks.get(0xf157)(c) === 'wait') { engine.pc = k; c.pc = 0xa7ae; return 'wait'; }
402
+ if (c.a === 0x0d) break;
403
+ s.typed.push(c.a);
404
+ if (s.typed.length >= 0x59) { inputting = null; throw new BasicError('?STRING TOO LONG ERROR'); }
405
+ }
406
+ chrout(0x0d);
407
+ s.text = s.typed; s.typed = []; s.at = 0; s.fresh = true;
408
+ if (!s.text.length && s.n === 0) { inputting = null; engine.ended = true; engine.pc = k + 1; return undefined; }
409
+ }
410
+ const t = s.text, skip = () => { while (t[s.at] === 0x20) s.at++; };
411
+ if (s.n === st.targets.length) {
412
+ if (s.at < t.length) print('?EXTRA IGNORED\r');
413
+ inputting = null; engine.pc = k + 1; return undefined;
414
+ }
415
+ // this variable's item, or the line run out: another
416
+ // (a line just read is read from, empty or not: an empty one gives 0, or "")
417
+ if (s.n > 0 && !s.fresh && (s.at >= t.length || t[s.at] === 0x3a)) { print('?? '); s.text = null; continue; }
418
+ if (!s.fresh && t[s.at] === 0x2c) s.at++; // past the comma (a new line's first comma is an empty item)
419
+ s.fresh = false;
420
+ skip();
421
+ const target = st.targets[s.n], string = target.n.endsWith('$');
422
+ let value;
423
+ if (string) {
424
+ let v = '';
425
+ if (t[s.at] === 0x22) { s.at++; while (s.at < t.length && t[s.at] !== 0x22) v += String.fromCharCode(t[s.at++]); if (t[s.at] === 0x22) s.at++; skip(); }
426
+ else while (s.at < t.length && t[s.at] !== 0x2c && t[s.at] !== 0x3a) v += String.fromCharCode(t[s.at++]);
427
+ value = v;
428
+ } else {
429
+ const n = scanNumber(String.fromCharCode(...t), s.at);
430
+ s.at = n.end; value = val(n.text);
431
+ }
432
+ if (s.at < t.length && t[s.at] !== 0x2c && t[s.at] !== 0x3a) { print('?REDO FROM START\r'); inputting = null; return input(st, k); }
433
+ if (target.k === 'elem') basic.setElem(target.n, target.i.map(e => evaluator(e)(basic)), value); else basic.set(target.n, value);
434
+ s.n++;
435
+ }
436
+ }
437
+ function breakIn(line) { print('\rBREAK'); if (line !== undefined && line >> 8 !== 0xff) print(' IN ' + line); }
438
+ // A line run at once (direct mode): its statements after the program's (basic-lang.js
439
+ // `direct`), on the variables the program left, or a BASIC of its own the first time; READY at
440
+ // its end, as at the program's
441
+ function runDirect() {
442
+ if (!basic) { basic = new Basic(engine, { quiet: true }); engine.basic = basic; }
443
+ const all = direct(m.ram, r(0x2b) | r(0x2c) << 8, 0x200);
444
+ all[all.findIndex(s => s.label === 'direct')].label = DIRECT;
445
+ engine.pc = compile(all);
446
+ c.pc = 0xa7ae; return false;
447
+ }
448
+ // LIST ($A69C): from the first line at the first number or after, each a RETURN, then (past
449
+ // the last number, the RETURN printed first, as the ROM prints it, and no more) its number, a
450
+ // space and its text as LIST gives it (basic-lang.js `list`); NEW ($A642): no program, and
451
+ // the variables cleared
452
+ engine.routines['basic-list'] = (e, [from, to]) => {
453
+ for (const l of programLines(m.ram, r(0x2b) | r(0x2c) << 8)) {
454
+ if (l.n < from) continue;
455
+ chrout(0x0d);
456
+ if (l.n > to) break;
457
+ print(l.n + ' '); list(l.bytes).forEach(ch => chrout(ch));
458
+ }
459
+ };
460
+ engine.routines['basic-new'] = e => {
461
+ const at = r(0x2b) | r(0x2c) << 8; w(at, 0); w(at + 1, 0);
462
+ for (const v of [0x2d, 0x2f, 0x31]) { w(v, at + 2); w(v + 1, (at + 2) >> 8); }
463
+ w(0x33, r(0x37)); w(0x34, r(0x38));
464
+ basic?.clr(); cont = null; e.ended = true;
465
+ };
466
+ // SAVE ($E156) and VERIFY ($E165): the program, TXTTAB to VARTAB, to the drive's file, or held
467
+ // to it; in direct mode the KERNAL's messages (SAVING, SEARCHING FOR, VERIFYING), and after a
468
+ // VERIFY, BASIC's OK, or ?VERIFY ERROR where a byte differs. Device 8 only: the tape is not
469
+ // moonbase's.
470
+ const file = (name, device) => {
471
+ if (device !== 8) { if (device === 1) throw new Error('the tape is not handled'); throw new BasicError('?DEVICE NOT PRESENT ERROR'); }
472
+ if (!name.length) throw new BasicError('?MISSING FILE NAME ERROR');
473
+ return Uint8Array.from(name, ch => ch.charCodeAt(0));
474
+ };
475
+ const directMode = () => r(0x3a) === 0xff;
476
+ engine.routines['basic-save'] = (e, [name = '', device = 1]) => {
477
+ const n = file(name, device), from = r(0x2b) | r(0x2c) << 8, to = r(0x2d) | r(0x2e) << 8;
478
+ if (directMode()) print('\rSAVING ' + name);
479
+ const body = new Uint8Array(to - from + 2); body[0] = from & 0xff; body[1] = from >> 8;
480
+ for (let a = from; a < to; a++) body[a - from + 2] = r(a);
481
+ m.drive.save(n, body);
482
+ };
483
+ engine.routines['basic-verify'] = (e, [name = '', device = 1, sa = 0]) => {
484
+ const n = file(name, device);
485
+ if (directMode()) print('\rSEARCHING FOR ' + name);
486
+ const raw = m.drive.load(n);
487
+ if (!raw || raw.length < 2) throw new BasicError('?FILE NOT FOUND ERROR');
488
+ if (directMode()) print('\rVERIFYING');
489
+ const at = sa ? raw[0] | raw[1] << 8 : r(0x2b) | r(0x2c) << 8;
490
+ for (let k = 2; k < raw.length; k++) if (r(at + k - 2) !== raw[k]) throw new BasicError('?VERIFY ERROR');
491
+ print('\rOK\r');
492
+ };
493
+ engine.routines['basic-cont'] = e => { if (cont === null) throw new BasicError('?CAN\'T CONTINUE ERROR'); e.pc = cont; };
494
+ function go() {
495
+ if (getting) {
496
+ const { k, t } = getting; getting = null;
497
+ const ch = c.a, s = ch ? String.fromCharCode(ch) : '';
498
+ basic.set(t.n, t.n.endsWith('$') ? s : (ch ? Number(s) || 0 : 0));
499
+ engine.pc = k + 1;
500
+ }
501
+ for (let i = 0; i < 100000; i++) {
502
+ // (so many statements a frame where the page asks, m.basicPace, as a C64's BASIC takes its
503
+ // time: a wait on the clock, or a FOR counted out for a pause, as long as on the machine)
504
+ if (m.basicPace) { if (m.frame !== paced) { paced = m.frame; budget = m.basicPace; } if (budget-- <= 0) return 'wait'; }
505
+ const k = engine.pc;
506
+ if (engine.ended || k >= program.steps.length) {
507
+ // (an END or STOP in the program leaves CONT the step after it)
508
+ if (engine.ended && /^(end|stop)$/.test(statements[k - 1]?.k) && lineOf[k - 1] !== 0xffff) cont = k;
509
+ // (STOP: BREAK, and IN its line in a program, as BASIC's error path prints it, $A469)
510
+ if (engine.ended && statements[k - 1]?.k === 'stop') breakIn(lineOf[k - 1]);
511
+ print('\rREADY.\r'); c.sp = 0xff; c.pc = 0xa480; return false;
512
+ }
513
+ const st = statements[k];
514
+ if (lineOf[k] !== undefined) { w(0x39, lineOf[k]); w(0x3a, lineOf[k] >> 8); }
515
+ // GET: GETIN through the processor, from BASIC's ROM
516
+ // the RUN/STOP key, as BASIC's ISCNTC looks before each statement: BREAK IN the line
517
+ if (r(0x91) === 0x7f && waiting !== k) { w(0xc6, 0); cont = k; breakIn(lineOf[k]); print('\rREADY.\r'); c.sp = 0xff; c.pc = 0xa480; return false; }
518
+ waiting = null;
519
+ if (st.k === 'input-keys') { const r1 = input(st, k); if (r1 !== undefined) return r1; continue; }
520
+ if (st.k === 'get') { w(0x13, 0); getting = { k, t: st.t }; c.push((0xa7ae - 1) >> 8); c.push((0xa7ae - 1) & 0xff); c.pc = 0xffe4; return false; }
521
+ engine.pc = k + 1;
522
+ let result;
523
+ try { inStatement = true; result = program.run[k](engine, program.steps); }
524
+ catch (err) {
525
+ if (!(err instanceof BasicError) || !printError(fromRom, err.message, err.basicLine ?? lineOf[k] ?? basic.at)) throw err;
526
+ c.sp = 0xff; c.pc = 0xa480; return false;
527
+ }
528
+ finally { inStatement = false; }
529
+ if (result === YIELD) return false;
530
+ // WAIT, its bits not come: again on the next frame, as the ROM's loop ($B82D) goes on round,
531
+ // RUN/STOP not looked at
532
+ if (st.k === 'wait' && engine.pc === k) { waiting = k; c.pc = 0xa7ae; return 'wait'; }
533
+ }
534
+ return 'wait';
535
+ }
536
+ }
537
+
package/saves.js ADDED
@@ -0,0 +1,10 @@
1
+ // What a Commodore 64 game SAVEs to its disk, kept on the player's disk (`disk`, opened before the machine
2
+ // starts: { list(), read(name), write(name, bytes) }), each file by its name as the 1541 has it, its raw
3
+ // bytes, the load address first: put back in the drive (drive.js `saved`, which shadows the disk's own
4
+ // files) when the game starts again, and each file saved written there as it is saved.
5
+ //
6
+ // keepDrive(drive, disk)
7
+ export function keepDrive(drive, disk) {
8
+ for (const f of disk.list()) drive.saved.set(f.name, disk.read(f.name));
9
+ drive.keep = (name, raw) => disk.write(name, raw);
10
+ }
package/sid.js ADDED
@@ -0,0 +1,49 @@
1
+ // The SID, as far as a square-wave era of school software used it: three voices, each an
2
+ // oscillator at a 16-bit frequency with a triangle, sawtooth, pulse or noise wave, gated by an
3
+ // attack, decay, sustain and release envelope, mixed under the master volume. Ring modulation,
4
+ // sync and the filter are not here. The machine keeps each register write with its cycle, and
5
+ // `render` turns a frame's writes into samples at the sound card's rate.
6
+ const CLOCK = 985248;
7
+ const ATTACK = [2, 8, 16, 24, 38, 56, 68, 80, 100, 250, 500, 800, 1000, 3000, 5000, 8000]; // ms
8
+ const DECAY = [6, 24, 48, 72, 114, 168, 204, 240, 300, 750, 1500, 2400, 3000, 9000, 15000, 24000];
9
+
10
+ export class SID {
11
+ constructor(rate) {
12
+ this.rate = rate; this.r = new Uint8Array(0x20);
13
+ this.v = [0, 1, 2].map(() => ({ phase: 0, env: 0, stage: 'off', noise: 0x7ffff8, noiseOut: 0 }));
14
+ }
15
+ write(reg, val) {
16
+ const prev = this.r[reg]; this.r[reg] = val;
17
+ if (reg % 7 === 4 && reg < 21) { const v = this.v[(reg / 7) | 0]; if ((val & 1) && !(prev & 1)) v.stage = 'attack'; else if (!(val & 1) && (prev & 1)) v.stage = 'release'; }
18
+ }
19
+ // `n` samples, with the writes in `events` as [sampleIndex, reg, val] applied in order
20
+ render(n, events) {
21
+ const out = new Float32Array(n);
22
+ let e = 0;
23
+ for (let i = 0; i < n; i++) {
24
+ while (e < events.length && events[e][0] <= i) { this.write(events[e][1], events[e][2]); e++; }
25
+ let mix = 0;
26
+ for (let k = 0; k < 3; k++) {
27
+ const b = k * 7, v = this.v[k], r = this.r;
28
+ const freq = r[b] | r[b + 1] << 8, ctrl = r[b + 4];
29
+ // the envelope, in levels of 0 to 1 per sample
30
+ const a = ATTACK[r[b + 5] >> 4] / 1000 * this.rate, d = DECAY[r[b + 5] & 15] / 1000 * this.rate, sus = (r[b + 6] >> 4) / 15, rel = DECAY[r[b + 6] & 15] / 1000 * this.rate;
31
+ if (v.stage === 'attack') { v.env += 1 / Math.max(1, a); if (v.env >= 1) { v.env = 1; v.stage = 'decay'; } }
32
+ else if (v.stage === 'decay') { v.env -= (1 - sus) / Math.max(1, d) * 3; if (v.env <= sus) { v.env = sus; v.stage = 'sustain'; } }
33
+ else if (v.stage === 'release') { v.env -= 1 / Math.max(1, rel) * 3; if (v.env <= 0) { v.env = 0; v.stage = 'off'; } }
34
+ if (v.env <= 0 && v.stage !== 'attack') continue;
35
+ // the oscillator: a 24-bit accumulator, as the chip's, stepped by the frequency
36
+ const step = freq * CLOCK / 16777216 / this.rate; // cycles of the wave per sample
37
+ const before = v.phase; v.phase = (v.phase + step) % 1;
38
+ let wave = 0;
39
+ if (ctrl & 0x10) wave += v.phase < 0.5 ? v.phase * 4 - 1 : 3 - v.phase * 4; // triangle
40
+ if (ctrl & 0x20) wave += v.phase * 2 - 1; // sawtooth
41
+ if (ctrl & 0x40) { const pw = (r[b + 2] | (r[b + 3] & 15) << 8) / 4096; wave += v.phase < pw ? 1 : -1; } // pulse
42
+ if (ctrl & 0x80) { if (v.phase < before) { const bit = ((v.noise >> 22) ^ (v.noise >> 17)) & 1; v.noise = ((v.noise << 1) | bit) & 0x7fffff; v.noiseOut = (((v.noise >> 22) & 1) << 7 | ((v.noise >> 20) & 1) << 6 | ((v.noise >> 16) & 1) << 5 | ((v.noise >> 13) & 1) << 4 | ((v.noise >> 11) & 1) << 3 | ((v.noise >> 7) & 1) << 2 | ((v.noise >> 4) & 1) << 1 | ((v.noise >> 2) & 1)) / 128 - 1; } wave += v.noiseOut; }
43
+ mix += wave * v.env;
44
+ }
45
+ out[i] = mix * (this.r[0x18] & 15) / 15 * 0.2;
46
+ }
47
+ return out;
48
+ }
49
+ }
package/start.js ADDED
@@ -0,0 +1,33 @@
1
+ // How a C64 game starts, in a page and in tests that start every game as the page does: which
2
+ // ROMs it runs on, a player page's ?roms=<choice> (/play/<slug>/original/, in the preview),
3
+ // (none), moonbase moonbase's, characters and all (rom.js): the default, as moonbase.au
4
+ // has it
5
+ // commodore Commodore's, where this machine has them: a flag, off unless asked for,
6
+ // kept as everyone else keeps them, to compare
7
+ // moonbase-petscii moonbase's, with Commodore's character ROM, where this machine has it
8
+ //
9
+ // await c64Roms(choice, slug, load) the roms for new C64(game, roms), or null where Commodore's
10
+ // are asked for and cannot be had; load(name) gives a ROM's
11
+ // bytes (<name>.bin), or throws where it is not there
12
+ // typed(m, game) what is typed once BASIC is ready: LOAD"<start>",8,1,
13
+ // and RUN for a BASIC program, one loaded at $0801 (ZOOPAK
14
+ // loads over the stack and starts itself, and a RUN typed
15
+ // after it would reach its menu as a stray RETURN)
16
+ import { standIn } from './rom.js';
17
+
18
+ export const ROM_CHOICES = [null, 'commodore', 'moonbase-petscii'];
19
+
20
+ export async function c64Roms(choice, slug, load) {
21
+ if (choice !== 'commodore' && choice !== 'moonbase-petscii') return standIn();
22
+ try {
23
+ if (choice === 'moonbase-petscii') return { ...standIn(), chargen: await load('chargen') };
24
+ return { kernal: await load('kernal'), basic: await load('basic'), chargen: await load('chargen') };
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export function typed(m, game) {
31
+ const raw = m.drive.find(Uint8Array.from(game.start, c => c.charCodeAt(0)))?.raw;
32
+ return 'LOAD"' + game.start + '",8,1\r' + (raw && (raw[0] | raw[1] << 8) === 0x801 ? 'RUN\r' : '');
33
+ }