@moonbase-machines/c64 0.0.0-stage → 0.5.1

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/index.js ADDED
@@ -0,0 +1,27 @@
1
+ // @moonbase-machines/c64: A Commodore 64, every module by name, for one file (a bundle) or to take the
2
+ // whole package at once: import { machine } from '@moonbase-machines/c64'. Each module can be imported
3
+ // on its own too: '@moonbase-machines/c64/<module>.js'.
4
+ export * as basicCompile from './basic-compile.js';
5
+ export * as basicList from './basic-list.js';
6
+ export * as basicLang from './basic-lang.js';
7
+ export * as basicRom from './basic-rom.js';
8
+ export * as basic from './basic.js';
9
+ export * as browser from './browser.js';
10
+ export * as d64 from './d64.js';
11
+ export * as drive from './drive.js';
12
+ export * as editor from './editor.js';
13
+ export * as float from './float.js';
14
+ export * as glyphHash from './glyph-hash.js';
15
+ export * as glyphHashes from './glyph-hashes.js';
16
+ export * as kernal from './kernal.js';
17
+ export * as keys from './keys.js';
18
+ export * as machine from './machine.js';
19
+ export * as numbers from './numbers.js';
20
+ export * as paint from './paint.js';
21
+ export * as petscii from './petscii.js';
22
+ export * as printer from './printer.js';
23
+ export * as rom from './rom.js';
24
+ export * as saves from './saves.js';
25
+ export * as sid from './sid.js';
26
+ export * as start from './start.js';
27
+ export * as vic from './vic.js';
package/kernal.js ADDED
@@ -0,0 +1,319 @@
1
+ // The Commodore 64's KERNAL as far as Zoopak calls it, in JavaScript, at the addresses it calls
2
+ // it: the remake's counterpart of @moonbase-machines/apple2e/rom.js, without Commodore's ROM. Each routine
3
+ // takes what the KERNAL took in the registers and the zero page, and leaves what it left there.
4
+ //
5
+ // install({ cpu, read, write, drive, loaded, call, log })
6
+ // cpu a stand-in for the processor: its registers, and `hooks`, which this fills
7
+ // read, write memory as the processor sees it, the I/O page at $D000 by the 6510's port
8
+ // drive the serial bus (printer.js `bus`): a 1541 (drive.js), its files by
9
+ // name, its command channel and its memory, and a printer, device 4, if it is on
10
+ // loaded told of each LOAD: the name, and where the file went
11
+ // call runs the game's own code at an address, to its return: a routine the KERNAL calls
12
+ // through a vector the game has pointed at it (Tell-A-Tale's keyboard decode)
13
+ // held the key held down on the page, as the keyboard's matrix shows it to the KERNAL's
14
+ // scan ({ code, shift }, matrixOf), or none
15
+ //
16
+ // Text out (CHROUT) is the screen editor's part the games use: a character at the cursor, in the
17
+ // colour at $0286 and reversed while $C7 says so; RETURN, CLR and HOME, the cursor keys, RVS ON
18
+ // and OFF, DEL, the colours and the two character sets. A control code it has no use for is
19
+ // nothing, as on the machine. The screen is where $0288 says, the colours at $D800.
20
+ import { editor } from './editor.js';
21
+
22
+ export const VIC = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9b, 0x37, 0x00, 0x00, 0x00, 0x08, 0x00,
23
+ 0x14, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x06, 0x01, 0x02, 0x03, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x4c]; // $D000 to $D02E, as CINT sets them
24
+ export const VECTORS = [0xea31, 0xfe66, 0xfe47, 0xf34a, 0xf291, 0xf20e, 0xf250, 0xf333, 0xf157, 0xf1ca, 0xf6ed, 0xf13e, 0xf32f, 0xfe66, 0xf4a5, 0xf5ed]; // $0314 on, as RESTOR puts them
25
+ const COLOURS = { 0x90: 0, 0x05: 1, 0x1c: 2, 0x9f: 3, 0x9c: 4, 0x1e: 5, 0x1f: 6, 0x9e: 7, 0x81: 8, 0x95: 9, 0x96: 10, 0x97: 11, 0x98: 12, 0x99: 13, 0x9a: 14, 0x9b: 15 };
26
+
27
+ // a PETSCII character as the screen code that shows it
28
+ export function screenCode(p) {
29
+ if (p >= 0x20 && p < 0x40) return p;
30
+ if (p >= 0x40 && p < 0x60) return p - 0x40;
31
+ if (p >= 0x60 && p < 0x80) return p - 0x20;
32
+ if (p >= 0xa0 && p < 0xc0) return p - 0x40;
33
+ if (p >= 0xc0 && p < 0xff) return p - 0x80;
34
+ return p === 0xff ? 0x5e : 0x20;
35
+ }
36
+
37
+ // The KERNAL's keyboard tables, which a game reads in the ROM: where each of the four is ($EB79:
38
+ // unshifted, shifted, Commodore key, CTRL), the character of each key of the keyboard's matrix
39
+ // unshifted (Find Me looks a character up in it, for the key to hold down), shifted (BASIC
40
+ // typed with the shift keys), with the Commodore key (the keycaps' left graphics) and with CTRL
41
+ // (the colours, RVS ON and OFF, and the control codes). `kernalByte` gives the byte at an address
42
+ // of the ROM, or undefined where it is not these. (They are the ROM's own bytes.)
43
+ const UNSHIFTED = [
44
+ 0x14, 0x0d, 0x1d, 0x88, 0x85, 0x86, 0x87, 0x11, // DEL, RETURN, cursor right, F7, F1, F3, F5, cursor down
45
+ 0x33, 0x57, 0x41, 0x34, 0x5a, 0x53, 0x45, 0x01, // 3 W A 4 Z S E, left shift
46
+ 0x35, 0x52, 0x44, 0x36, 0x43, 0x46, 0x54, 0x58, // 5 R D 6 C F T X
47
+ 0x37, 0x59, 0x47, 0x38, 0x42, 0x48, 0x55, 0x56, // 7 Y G 8 B H U V
48
+ 0x39, 0x49, 0x4a, 0x30, 0x4d, 0x4b, 0x4f, 0x4e, // 9 I J 0 M K O N
49
+ 0x2b, 0x50, 0x4c, 0x2d, 0x2e, 0x3a, 0x40, 0x2c, // + P L - . : @ ,
50
+ 0x5c, 0x2a, 0x3b, 0x13, 0x01, 0x3d, 0x5e, 0x2f, // pound * ; HOME, right shift, = up-arrow /
51
+ 0x31, 0x5f, 0x04, 0x32, 0x20, 0x02, 0x51, 0x03, // 1, left arrow, CTRL, 2, space, Commodore key, Q, RUN/STOP
52
+ ];
53
+ // and with a shift key down: the second character of each key, a letter's graphic
54
+ const SHIFTED = [
55
+ 0x94, 0x8d, 0x9d, 0x8c, 0x89, 0x8a, 0x8b, 0x91, // INST, shift RETURN, cursor left, F8, F2, F4, F6, cursor up
56
+ 0x23, 0xd7, 0xc1, 0x24, 0xda, 0xd3, 0xc5, 0x01, // # and the graphics of W A, $, Z S E, left shift
57
+ 0x25, 0xd2, 0xc4, 0x26, 0xc3, 0xc6, 0xd4, 0xd8, // % R D & C F T X
58
+ 0x27, 0xd9, 0xc7, 0x28, 0xc2, 0xc8, 0xd5, 0xd6, // ' Y G ( B H U V
59
+ 0x29, 0xc9, 0xca, 0x30, 0xcd, 0xcb, 0xcf, 0xce, // ) I J 0 M K O N
60
+ 0xdb, 0xd0, 0xcc, 0xdd, 0x3e, 0x5b, 0xba, 0x3c, // + P L - > [ @ <
61
+ 0xa9, 0xc0, 0x5d, 0x93, 0x01, 0x3d, 0xde, 0x3f, // pound * ] CLR, right shift, = pi ?
62
+ 0x21, 0x5f, 0x04, 0x22, 0xa0, 0x02, 0xd1, 0x83, // ! left arrow, CTRL, " shift space, Commodore key, Q, LOAD and RUN
63
+ ];
64
+ // with the Commodore key down: a letter's other graphic, the left one on its keycap; the digits
65
+ // 1 to 8 the second eight colours
66
+ const COMMODORE = [
67
+ 0x94, 0x8d, 0x9d, 0x8c, 0x89, 0x8a, 0x8b, 0x91, // INST, shift RETURN, cursor left, F8, F2, F4, F6, cursor up
68
+ 0x96, 0xb3, 0xb0, 0x97, 0xad, 0xae, 0xb1, 0x01, // light red, the graphics of W A, dark grey, Z S E, left shift
69
+ 0x98, 0xb2, 0xac, 0x99, 0xbc, 0xbb, 0xa3, 0xbd, // grey, R D, light green, C F T X
70
+ 0x9a, 0xb7, 0xa5, 0x9b, 0xbf, 0xb4, 0xb8, 0xbe, // light blue, Y G, light grey, B H U V
71
+ 0x29, 0xa2, 0xb5, 0x30, 0xa7, 0xa1, 0xb9, 0xaa, // ) I J 0 M K O N
72
+ 0xa6, 0xaf, 0xb6, 0xdc, 0x3e, 0x5b, 0xa4, 0x3c, // + P L - > [ @ <
73
+ 0xa8, 0xdf, 0x5d, 0x93, 0x01, 0x3d, 0xde, 0x3f, // pound * ] CLR, right shift, = pi ?
74
+ 0x81, 0x5f, 0x04, 0x95, 0xa0, 0x02, 0xab, 0x83, // orange, left arrow, CTRL, brown, shift space, Commodore key, Q, LOAD and RUN
75
+ ];
76
+ // with CTRL down: the digits 1 to 8 the first eight colours, 9 and 0 RVS ON and OFF, the letters
77
+ // their control codes (A 1 to Z 26); $FF where a key gives nothing
78
+ const CTRL = [
79
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // DEL, RETURN, cursor right, F7, F1, F3, F5, cursor down: nothing
80
+ 0x1c, 0x17, 0x01, 0x9f, 0x1a, 0x13, 0x05, 0xff, // red, W A, cyan, Z S, white (E), left shift
81
+ 0x9c, 0x12, 0x04, 0x1e, 0x03, 0x06, 0x14, 0x18, // purple, R (RVS ON) D, green, C F T X
82
+ 0x1f, 0x19, 0x07, 0x9e, 0x02, 0x08, 0x15, 0x16, // blue, Y G, yellow, B H U V
83
+ 0x12, 0x09, 0x0a, 0x92, 0x0d, 0x0b, 0x0f, 0x0e, // RVS ON (9), I J, RVS OFF (0), M K O N
84
+ 0xff, 0x10, 0x0c, 0xff, 0xff, 0x1b, 0x00, 0xff, // + P L - . : @ ,
85
+ 0x1c, 0xff, 0x1d, 0xff, 0xff, 0x1f, 0x1e, 0xff, // pound * ; HOME, right shift, = up-arrow /
86
+ 0x90, 0x06, 0xff, 0x05, 0xff, 0xff, 0x11, 0xff, // black (1), left arrow, CTRL, white (2), space, Commodore key, Q, RUN/STOP
87
+ ];
88
+ const DECODE = [UNSHIFTED, SHIFTED, COMMODORE, CTRL]; // the tables at $EB81, $EBC2, $EC03 and $EC78
89
+ const TABLES = [0xeb81, 0xebc2, 0xec03, 0xec78];
90
+ export const KEY_TABLES = [0xeb79, 0xecb9]; // the addresses they take, the four and where they are
91
+ export function kernalByte(at) {
92
+ if (at >= 0xeb79 && at < 0xeb81) return TABLES[(at - 0xeb79) >> 1] >> ((at & 1) ? 0 : 8) & 0xff;
93
+ const t = TABLES.findIndex(f => at >= f && at <= f + 0x40), code = at - TABLES[t];
94
+ const v = t < 0 ? undefined : code === 0x40 ? 0xff : DECODE[t][code];
95
+ if (v === undefined && at >= KEY_TABLES[0] && at < KEY_TABLES[1]) throw new Error(`the KERNAL's byte at $${at.toString(16).toUpperCase()}: not handled`);
96
+ return v;
97
+ }
98
+
99
+ // A character as the key of the matrix that types it, and the shift with it: the cursor keys,
100
+ // RETURN, DEL and the keys of the unshifted table; or null (a character typed only into the buffer)
101
+ export function matrixOf(p) {
102
+ const shifted = { 0x9d: 2, 0x91: 7 };
103
+ if (shifted[p] !== undefined) return { code: shifted[p], shift: 1 };
104
+ const code = UNSHIFTED.indexOf(p);
105
+ return code < 0 || [0x01, 0x02, 0x03, 0x04].includes(p) ? null : { code, shift: 0 };
106
+ }
107
+
108
+ export function install({ cpu: c, read, write, drive, loaded = () => {}, call, held = () => null, log = () => {} }) {
109
+ const z = a => read(a), zw = (a, v) => write(a, v & 0xff);
110
+ // the screen editor, as the KERNAL's (editor.js): CHROUT's characters for the screen,
111
+ // the screen cleared, the pointers set; its scroll reads the keyboard's CTRL row as the KERNAL's does
112
+ const ed = editor({ read, write, keyRow: column => { write(0xdc00, column); const v = read(0xdc01); write(0xdc00, 0x7f); return v; } });
113
+ const chrout = ch => ed.chrout(ch), clear = () => ed.clear();
114
+ let typing = false;
115
+ const status = v => zw(0x90, v);
116
+ // Files, as the KERNAL keeps them: a table of up to ten, each its logical number ($0259), its
117
+ // device ($0263) and its secondary address ($026D), how many at $98; the input's device at $99
118
+ // and the output's at $9A. Device 8 is the drive, through the bus; the keyboard (0) and the
119
+ // screen (3) are the KERNAL's own. Any other device on the bus (4 on) is not there: its LISTEN
120
+ // or TALK leaves the status $80, DEVICE NOT PRESENT, and CHKOUT or CHKIN to it fails (5) and
121
+ // leaves the output on the screen, as Goldfields' check for a printer finds ($3EB3).
122
+ const files = () => z(0x98), entry = la => { for (let k = 0; k < files(); k++) if (z(0x259 + k) === la) return k; return -1; };
123
+ const open = () => {
124
+ const la = z(0xb8), fa = z(0xba), sa = z(0xb9);
125
+ if (entry(la) >= 0) { c.a = 2; c.c = 1; return; } // FILE OPEN
126
+ if (files() >= 10) { c.a = 1; c.c = 1; return; } // TOO MANY FILES
127
+ const k = files(); zw(0x259 + k, la); zw(0x263 + k, fa); zw(0x26d + k, sa | 0x60); zw(0x98, k + 1);
128
+ if (fa >= 4) {
129
+ status(0);
130
+ if (z(0xb7)) {
131
+ if (!drive.listen(fa)) { status(0x80); c.a = 5; c.c = 1; return; }
132
+ drive.second((sa | 0x60) | 0xf0); const at = z(0xbb) | z(0xbc) << 8; for (let i = 0; i < z(0xb7); i++) drive.ciout(read(at + i)); drive.unlsn();
133
+ }
134
+ }
135
+ c.c = 0;
136
+ };
137
+ const close = () => {
138
+ const k = entry(c.a); if (k < 0) { c.c = 0; return; }
139
+ const fa = z(0x263 + k), sa = z(0x26d + k);
140
+ if (fa >= 4) { const there = drive.listen(fa); status(there ? 0 : 0x80); if (there) { drive.second((sa & 0xef) | 0xe0); drive.unlsn(); } }
141
+ for (let i = k; i < files() - 1; i++) { zw(0x259 + i, z(0x25a + i)); zw(0x263 + i, z(0x264 + i)); zw(0x26d + i, z(0x26e + i)); }
142
+ zw(0x98, files() - 1); c.c = 0;
143
+ };
144
+ const channel = output => {
145
+ const k = entry(c.x); if (k < 0) { c.a = 3; c.c = 1; return; } // FILE NOT OPEN
146
+ const fa = z(0x263 + k), sa = z(0x26d + k);
147
+ if (fa >= 4) {
148
+ const there = output ? drive.listen(fa) : drive.talk(fa); status(there ? 0 : 0x80);
149
+ if (!there) { c.a = 5; c.c = 1; return; }
150
+ if (output) drive.second(sa); else drive.tksa(sa);
151
+ }
152
+ zw(output ? 0x9a : 0x99, fa); c.c = 0;
153
+ };
154
+ const clrchn = () => { if (z(0x9a) >= 4) drive.unlsn(); if (z(0x99) >= 4) drive.untlk(); zw(0x9a, 3); zw(0x99, 0); };
155
+ const hooks = {
156
+ 0xffd2: () => { if (z(0x9a) >= 4) drive.ciout(c.a); else chrout(c.a); c.c = 0; }, // CHROUT: to the screen, or a device on the bus (the drive's file, the printer)
157
+ 0xffc0: open, 0xffc3: close, 0xffc6: () => channel(false), 0xffc9: () => channel(true), // OPEN, CLOSE, CHKIN, CHKOUT
158
+ 0xffe7: () => { clrchn(); zw(0x98, 0); }, // CLALL
159
+ // CHRIN: from the keyboard, the line typed, a character a call (the screen editor's, its
160
+ // place begun once a call and not again while it waits for keys: `typing`); else a byte from
161
+ // the file on the drive, its status as it ends (none read once it is set: CHKIN clears it)
162
+ 0xffcf: () => {
163
+ if (z(0x99) === 0) {
164
+ if (!typing) { ed.begin(); typing = true; }
165
+ const ch = ed.input();
166
+ if (ch === null) return 'wait';
167
+ typing = false; c.a = ch; c.c = 0; return;
168
+ }
169
+ if (z(0x99) !== 8) throw new Error('CHRIN from device ' + z(0x99) + ': not handled');
170
+ // ($F1AD) with the status already set, a RETURN and no read; else ACPTR's byte
171
+ if (z(0x90)) { c.a = 0x0d; c.c = 0; return; }
172
+ const r = drive.acptr(); if (r) { c.a = r.byte; if (r.eoi) status(z(0x90) | 0x40); } else { c.a = 0x0d; status(z(0x90) | 0x42); } c.c = 0;
173
+ },
174
+ 0xfff0: () => { if (c.c) { c.x = z(0xd6); c.y = z(0xd3); } else { zw(0xd6, c.x); zw(0xd3, c.y); ed.setLine(); } }, // PLOT
175
+ 0xffcc: clrchn, 0xff90: () => { zw(0x9d, c.a); }, // CLRCHN, SETMSG
176
+ 0xffe1: () => { c.z = 0; }, // STOP: not pressed
177
+ 0xffb7: () => { c.a = z(0x90); c.nz(c.a); }, // READST
178
+ 0xffba: () => { zw(0xb8, c.a); zw(0xba, c.x); zw(0xb9, c.y); }, // SETLFS
179
+ 0xffbd: () => { zw(0xb7, c.a); zw(0xbb, c.x); zw(0xbc, c.y); }, // SETNAM
180
+ // LOAD: the file the name says from device 8, where it says or at X and Y; the end in X and Y
181
+ 0xffd5: () => {
182
+ const n = z(0xb7), from = z(0xbb) | z(0xbc) << 8, name = Uint8Array.from({ length: n }, (_, i) => z(from + i)), sa = z(0xb9);
183
+ const raw = z(0xba) === 8 ? drive.load(name) : null;
184
+ if (!raw || raw.length < 2 || c.a !== 0) { c.a = raw ? 4 : 4; c.c = 1; status(0x42); log('LOAD', String.fromCharCode(...name), 'not found'); return; }
185
+ const at = sa === 0 ? c.x | c.y << 8 : raw[0] | raw[1] << 8, body = raw.subarray(2);
186
+ for (let i = 0; i < body.length; i++) write(at + i, body[i]);
187
+ const end = at + body.length;
188
+ zw(0xae, end); zw(0xaf, end >> 8); status(0); c.x = end & 0xff; c.y = end >> 8; c.c = 0; c.a = 0;
189
+ loaded(String.fromCharCode(...name), at, end - 1);
190
+ },
191
+ // SAVE: from the address in the zero page at A to the one in X and Y
192
+ 0xffd8: () => {
193
+ const n = z(0xb7), at = z(0xbb) | z(0xbc) << 8, name = Uint8Array.from({ length: n }, (_, i) => z(at + i));
194
+ const from = z(c.a) | z(c.a + 1) << 8, to = c.x | c.y << 8, body = new Uint8Array(to - from + 2);
195
+ body[0] = from & 0xff; body[1] = from >> 8; for (let i = from; i < to; i++) body[i - from + 2] = read(i);
196
+ drive.save(name, body); c.c = 0; c.a = 0;
197
+ },
198
+ // the serial bus, answered by the drive as the machine's hooks answer it
199
+ 0xffb1: () => status(drive.listen(c.a) ? 0 : 0x80), 0xffb4: () => status(drive.talk(c.a) ? 0 : 0x80),
200
+ 0xff93: () => drive.second(c.a), 0xff96: () => drive.tksa(c.a), 0xffa8: () => drive.ciout(c.a),
201
+ 0xffae: () => drive.unlsn(), 0xffab: () => drive.untlk(),
202
+ 0xffa5: () => { const r = drive.acptr(); if (r) { c.a = r.byte; if (r.eoi) status(z(0x90) | 0x40); } else { c.a = 0x0d; status(z(0x90) | 0x42); } c.c = 0; },
203
+ // the clock at $A0 to $A2, which only the interrupt moves (UDTIM, a sixtieth of a second, and
204
+ // round again after 24 hours), and the keyboard's row with STOP, read as it moves: no key
205
+ 0xffdb: () => { zw(0xa2, c.a); zw(0xa1, c.x); zw(0xa0, c.y); }, 0xffde: () => { c.a = z(0xa2); c.x = z(0xa1); c.y = z(0xa0); },
206
+ 0xffea: () => {
207
+ let t = (z(0xa0) << 16 | z(0xa1) << 8 | z(0xa2)) + 1;
208
+ if (t >= 0x4f1a01) t = 0;
209
+ zw(0xa0, t >> 16); zw(0xa1, t >> 8); zw(0xa2, t); zw(0x91, read(0xdc01));
210
+ },
211
+ // CINT, as MENU calls it within the ROM ($FF5B): the VIC set up, the screen cleared
212
+ 0xff5b: () => { VIC.forEach((v, i) => write(0xd000 + i, v)); zw(0x286, z(0x286) || 14); zw(0x288, z(0x288) || 4); zw(0xc7, 0); clear(); },
213
+ // RESTOR ($FD15): the KERNAL's vectors back to its own
214
+ 0xfd15: () => VECTORS.forEach((v, i) => { zw(0x314 + 2 * i, v); zw(0x315 + 2 * i, v >> 8); }),
215
+ };
216
+ // The keyboard read (SCNKEY, $EA87), from the key it found down: its code in the matrix at $CB
217
+ // and the shift keys at $028D, as the KERNAL's decode takes them on from $EADD (where MENU's
218
+ // interrupt sends a cursor key it holds down for the joystick or the last key, $02). The key
219
+ // goes into the buffer when it is new, or held long enough to repeat (the cursor keys, space
220
+ // and DEL repeat, after 16 interrupts and then every 4). A key held on the page is down in the
221
+ // matrix (`held`); keys typed by a program go into the buffer, as the machine's typing does
222
+ // (machine.js).
223
+ // (or the game's own decode, where it has pointed $028F)
224
+ const decode = () => {
225
+ const own = z(0x28f) | z(0x290) << 8;
226
+ if (own !== 0xeb48) { call(own); return; }
227
+ const code = z(0xcb), shift = z(0x28d);
228
+ // (the table as the KERNAL's decode chooses it, $EB48: SHIFT the shifted, the Commodore key its
229
+ // own, CTRL with or without the others CTRL's; the Commodore key and SHIFT together switch the
230
+ // character set, once a press, unless $0291 locks it, and decode with the table before)
231
+ let table;
232
+ if (shift === 3) { switchSets(shift); table = Math.max(0, TABLES.indexOf(z(0xf5) | z(0xf6) << 8)); }
233
+ else { table = shift * 2 >= 8 ? 3 : shift; zw(0xf5, TABLES[table]); zw(0xf6, TABLES[table] >> 8); }
234
+ const key = code === 0x40 ? 0xff : DECODE[table][code];
235
+ const put = () => {
236
+ zw(0xc5, code); zw(0x28e, shift);
237
+ if (key !== 0xff && z(0xc6) < z(0x289)) { zw(0x277 + z(0xc6), key); zw(0xc6, z(0xc6) + 1); }
238
+ };
239
+ if (code !== z(0xc5)) { zw(0x28c, 0x10); put(); return; } // a new key: in at once, its repeat 16 interrupts on
240
+ const k = key & 0x7f, mode = z(0x28a);
241
+ if (!(mode & 0x80)) { // only these keys repeat, unless all do ($028A)
242
+ if (mode & 0x40) return; // (or none)
243
+ if (k === 0x7f) { put(); return; }
244
+ if (![0x14, 0x20, 0x1d, 0x11].includes(k)) return;
245
+ }
246
+ if (z(0x28c)) { zw(0x28c, z(0x28c) - 1); if (z(0x28c)) return; }
247
+ zw(0x28b, z(0x28b) - 1); if (z(0x28b)) return;
248
+ zw(0x28b, 4);
249
+ if (z(0xc6)) return; // a repeat only into an empty buffer
250
+ put();
251
+ };
252
+ // the scan: the key held down, decoded; or none (keys typed by a program come only through the
253
+ // buffer), which goes past the decode and its vector, and only notes that none is down
254
+ const scnkey = () => {
255
+ const h = held(), shift = h?.shift ?? 0; zw(0x28d, shift); zw(0xcb, h?.code ?? 0x40);
256
+ if (h && h.code !== null) decode(); else { switchSets(shift); zw(0xc5, 0x40); zw(0x28e, shift); }
257
+ };
258
+ // the Commodore key and SHIFT pressed together: the other character set, as $D018 has it
259
+ const switchSets = shift => { if (shift === 3 && z(0x28e) !== 3 && !(z(0x291) & 0x80)) write(0xd018, read(0xd018) ^ 2); };
260
+ hooks[0xeadd] = decode;
261
+ // The interrupt's own routine ($EA31, where $0314 points first): the clock, the cursor's blink
262
+ // (only while BASIC takes a line: $CC is 0), the cassette's motor off, the keyboard; and its end,
263
+ // which a game's own routine goes on to: $EA7B the keyboard read, $EA7E the CIA's interrupt
264
+ // answered, $EA81 the registers back and RTI, which are the remake's to put back
265
+ // ($EA34 the same without the clock, where Goldfields' boot points $0314 while it loads)
266
+ hooks[0xea31] = () => { hooks[0xffea](); hooks[0xea34](); };
267
+ hooks[0xea34] = () => {
268
+ // the cursor's blink, while $CC is 0: every 20 interrupts ($CD) the character at the cursor
269
+ // reversed, it and its colour kept ($CE, $0287) while it is ($CF), and put back after
270
+ if (!z(0xcc)) {
271
+ zw(0xcd, z(0xcd) - 1);
272
+ if (!z(0xcd)) {
273
+ zw(0xcd, 20);
274
+ const y = z(0xd3), at = (z(0xd1) | z(0xd2) << 8) + y, colour = 0xd800 + ((z(0xd1) | z(0xd2) << 8) & 0x3ff) + y;
275
+ const was = z(0xcf) & 1; zw(0xcf, z(0xcf) >> 1);
276
+ let x = z(0x287), a = read(at);
277
+ if (!was) { zw(0xcf, z(0xcf) + 1); zw(0xce, a); zw(0x287, read(colour)); x = z(0x286); a = z(0xce); }
278
+ write(at, a ^ 0x80); write(colour, x);
279
+ }
280
+ }
281
+ if (z(1) & 0x10) { zw(0xc0, 0); zw(1, z(1) | 0x20); } else if (!z(0xc0)) zw(1, z(1) & 0x1f);
282
+ scnkey();
283
+ };
284
+ hooks[0xea7b] = scnkey;
285
+ for (const a of [0xea7e, 0xea81]) hooks[a] = () => {};
286
+ // Two of BASIC's routines, which Tell-A-Tale calls to write a number: a whole number in $62 and
287
+ // $63 into the floating point accumulator (FLOATC, $BC49, X the exponent: $90 for 16 bits, C set
288
+ // for positive), and the accumulator as text (FOUT, entered at $BDDF): its sign at $00FF+Y (Y as
289
+ // FLOATC leaves it, 0), its digits after, a 0 at the end, and its address in A and Y. Another
290
+ // number than a whole one is not handled: it stops the remake.
291
+ hooks[0xbc49] = () => {
292
+ if (c.x !== 0x90 || !c.c) throw new Error('FLOATC with another exponent or sign: not handled');
293
+ let mant = z(0x62) << 24 | z(0x63) << 16, exp = c.x;
294
+ if (!mant) exp = 0; else while (!(mant & 0x80000000)) { mant = (mant << 1) >>> 0; exp--; }
295
+ zw(0x61, exp); zw(0x62, mant >>> 24); zw(0x63, mant >>> 16); zw(0x64, 0); zw(0x65, 0); zw(0x66, 0); zw(0x70, 0); c.y = 0;
296
+ };
297
+ hooks[0xbddf] = () => {
298
+ const exp = z(0x61), mant = (z(0x62) << 24 | z(0x63) << 16 | z(0x64) << 8 | z(0x65)) >>> 0, shift = 0xa0 - exp;
299
+ if (exp && (shift < 0 || shift > 32 || mant % 2 ** shift)) throw new Error('FOUT of a number not whole: not handled');
300
+ const n = exp ? mant / 2 ** shift : 0, sign = z(0x66) & 0x80 ? 0x2d : 0x20, text = String(n);
301
+ write(0xff + c.y, sign); zw(0x66, sign);
302
+ for (let i = 0; i < text.length; i++) write(0x100 + c.y + i, text.charCodeAt(i));
303
+ write(0x100 + c.y + text.length, 0);
304
+ c.a = 0x00; c.y = 0x01; c.x = 0;
305
+ };
306
+ // two of OPEN's and CLOSE's own steps for a file on the drive, which MENU's fast loader calls
307
+ // within the ROM: the secondary address and name sent ($F3D5), and the file closed ($F642)
308
+ hooks[0xf3d5] = () => {
309
+ if (z(0xb9) & 0x80 || !z(0xb7)) { c.c = 0; return; }
310
+ status(drive.listen(z(0xba)) ? 0 : 0x80); drive.second(z(0xb9) | 0xf0);
311
+ const at = z(0xbb) | z(0xbc) << 8; for (let i = 0; i < z(0xb7); i++) drive.ciout(z(at + i));
312
+ drive.unlsn(); c.c = z(0x90) & 0x80 ? 1 : 0;
313
+ };
314
+ hooks[0xf642] = () => { if (z(0xb9) & 0x80) return; const there = drive.listen(z(0xba)); status(there ? 0 : 0x80); if (there) { drive.second((z(0xb9) & 0xef) | 0xe0); drive.unlsn(); } c.c = 0; };
315
+ // and the routines the vectors lead to, which a game's own routine hands on to (MENU's CHROUT
316
+ // ends JMP $F1CA): the same
317
+ Object.assign(hooks, { 0xf1ca: hooks[0xffd2], 0xf4a5: hooks[0xffd5], 0xf5ed: hooks[0xffd8] });
318
+ for (const [a, f] of Object.entries(hooks)) c.hooks.set(+a, f);
319
+ }
package/keys.js ADDED
@@ -0,0 +1,21 @@
1
+ // The browser's keys as the C64's keyboard matrix, column then row, and the joystick in port 2
2
+ // on the arrow keys with the fire button on the left control key or shift.
3
+ export const MATRIX = {
4
+ Backspace: [0, 0], Delete: [0, 0], Enter: [0, 1], F7: [0, 3], F1: [0, 4], F3: [0, 5], F5: [0, 6],
5
+ '3': [1, 0], w: [1, 1], a: [1, 2], '4': [1, 3], z: [1, 4], s: [1, 5], e: [1, 6],
6
+ '5': [2, 0], r: [2, 1], d: [2, 2], '6': [2, 3], c: [2, 4], f: [2, 5], t: [2, 6], x: [2, 7],
7
+ '7': [3, 0], y: [3, 1], g: [3, 2], '8': [3, 3], b: [3, 4], h: [3, 5], u: [3, 6], v: [3, 7],
8
+ '9': [4, 0], i: [4, 1], j: [4, 2], '0': [4, 3], m: [4, 4], k: [4, 5], o: [4, 6], n: [4, 7],
9
+ '+': [5, 0], p: [5, 1], l: [5, 2], '-': [5, 3], '.': [5, 4], ':': [5, 5], '@': [5, 6], ',': [5, 7],
10
+ '£': [6, 0], '*': [6, 1], ';': [6, 2], Home: [6, 3], '=': [6, 5], '^': [6, 6], '/': [6, 7],
11
+ '1': [7, 0], _: [7, 1], '2': [7, 3], ' ': [7, 4], q: [7, 6], Escape: [7, 7],
12
+ };
13
+ // keys that need shift on the C64: the shifted character (or function key) and its unshifted key
14
+ export const SHIFTED = { '!': '1', '"': '2', '#': '3', '$': '4', '%': '5', '&': '6', "'": '7', '(': '8', ')': '9', '?': '/', '<': ',', '>': '.', '[': ':', ']': ';',
15
+ F2: 'F1', F4: 'F3', F6: 'F5', F8: 'F7' }; // (F2, F4, F6 and F8 are shift with F1, F3, F5 and F7 on the 64)
16
+ // the C64's own modifier keys, where VICE puts them: Control the Commodore key (and the
17
+ // joystick's fire too), Tab CTRL
18
+ export const MODIFIERS = { Control: [7, 5], Tab: [7, 2] };
19
+ export const JOYSTICK = { ArrowUp: 1, ArrowDown: 2, ArrowLeft: 4, ArrowRight: 8, Control: 16, Shift: 16, Alt: 16 };
20
+ // the arrow keys are also the cursor keys: down and right, and with shift, up and left
21
+ export const CURSOR = { ArrowDown: [0, 7, 0], ArrowRight: [0, 2, 0], ArrowUp: [0, 7, 1], ArrowLeft: [0, 2, 1] };
package/machine.js ADDED
@@ -0,0 +1,209 @@
1
+ // A Commodore 64, as far as the games use it: 64K of RAM with the KERNAL, BASIC and character
2
+ // ROMs banked over it by the 6510's port, the VIC-II's text and bitmap modes and sprites drawn
3
+ // a raster line at a time, its raster interrupt, the two CIAs' timers and the keyboard and
4
+ // joystick through the first, the SID's registers, and the 1541 in drive.js, answered when the
5
+ // KERNAL is asked to load or save and at the KERNAL's serial bus routines, so that the games'
6
+ // own talking to the drive works too. PAL: 312 lines of 63 cycles, 985,248 cycles a second.
7
+ import { CPU } from '@moonbase-machines/mos6502/cpu.js';
8
+ import { VIC } from './vic.js';
9
+ import { Drive } from './drive.js';
10
+ import { Printer, bus } from './printer.js';
11
+ import { install as standIn } from './rom.js';
12
+
13
+ const LINES = 312, CYCLES_PER_LINE = 63, DISPLAY_START = 15; // (the cycle of a line its first character is fetched at)
14
+
15
+ export class C64 {
16
+ constructor(game, roms, { log = () => {}, printer = new Printer({ on: false }) } = {}) {
17
+ this.game = game; this.log = log;
18
+ this.ram = new Uint8Array(0x10000);
19
+ this.kernal = roms.kernal; this.basic = roms.basic; this.chargen = roms.chargen;
20
+ this.color = new Uint8Array(0x400);
21
+ this.port = 0x37; this.portDir = 0x2f;
22
+ this.cia1 = new CIA(); this.cia2 = new CIA();
23
+ this.sid = new Uint8Array(0x20); this.sidWrites = []; // [cycle, register, value] since the last frame
24
+ this.vic = new VIC(this);
25
+ this.keys = new Uint8Array(8); // the keyboard matrix, a bit a key, set while held
26
+ this.joystick = 0; // port 2: up 1, down 2, left 4, right 8, fire 16
27
+ this.drive = new Drive(Uint8Array.from(atob(game.image), c => c.charCodeAt(0)));
28
+ this.printer = printer; // device 4, on the bus if switched on (printer.js)
29
+ this.halted = false; this.frame = 0; this.line = 0;
30
+ this.cpu = new CPU(this, { nmos: true, writeBack: true }); // the 6510 is an NMOS 6502, undocumented NOPs and all, and its read-modify-writes write twice
31
+ // the KERNAL's LOAD, once the name and device are set, and its SAVE; on Commodore's ROMs with
32
+ // their vectors ($0330, $0332) as the KERNAL sets them, its own code instead, on the bus
33
+ // below: its messages (SEARCHING FOR, LOADING, VERIFYING, SAVING) and VERIFY as it does them.
34
+ // A game's own loader at the vector (Zoopak's) is answered here.
35
+ const own = (vector, at) => !roms.standIn && this.read16(vector) === at;
36
+ this.cpu.hooks.set(0xf49e, () => own(0x330, 0xf4a5) ? false : this.load());
37
+ this.cpu.hooks.set(0xf5dd, () => own(0x332, 0xf5ed) ? false : this.save());
38
+ // the serial bus, at the routines the KERNAL's jump table leads to; the status at $90 says
39
+ // whether the device answered, and bit 6 marks the last byte of a file
40
+ const d = this.bus = bus(this.drive, this.printer), st = this.ram;
41
+ // (a device not there adds DEVICE NOT PRESENT to the status, as the KERNAL's $FE1C does; one
42
+ // that answers leaves it as it was: a VERIFY's mismatch, bit 4, kept through the CLOSE after)
43
+ this.cpu.hooks.set(0xed0c, c => { if (!d.listen(c.a)) st[0x90] |= 0x80; return true; });
44
+ this.cpu.hooks.set(0xed09, c => { if (!d.talk(c.a)) st[0x90] |= 0x80; return true; });
45
+ this.cpu.hooks.set(0xedb9, c => { d.second(c.a); return true; });
46
+ this.cpu.hooks.set(0xedc7, c => { d.tksa(c.a); return true; });
47
+ // (CIOUT ends CLC, as Commodore's does: BASIC's CHROUT, $E10C, takes a carry set as an error)
48
+ this.cpu.hooks.set(0xeddd, c => { d.ciout(c.a); c.c = 0; return true; });
49
+ this.cpu.hooks.set(0xedfe, () => { d.unlsn(); return true; });
50
+ this.cpu.hooks.set(0xedef, () => { d.untlk(); return true; });
51
+ this.cpu.hooks.set(0xee13, c => { const r = d.acptr(); if (r) { c.a = r.byte; if (r.eoi) st[0x90] |= 0x40; } else { c.a = 0x0d; st[0x90] |= 0x42; } c.c = 0; return true; });
52
+ // Commodore's ROMs, or moonbase's in their place (rom.js)
53
+ if (roms.standIn) standIn(this);
54
+ this.reset();
55
+ }
56
+
57
+ reset() { this.port = 0x37; this.portDir = 0x2f; this.cpu.pc = this.read16(0xfffc); this.cpu.sp = 0xfd; this.cpu.i = 1; }
58
+ read16(a) { return this.read(a) | this.read(a + 1) << 8; }
59
+
60
+ // what the CPU sees, by the port's LORAM, HIRAM and CHAREN bits
61
+ read(a) {
62
+ if (a === 0) return this.portDir;
63
+ if (a === 1) return (this.port & this.portDir) | (~this.portDir & 0x17) & 0xff | 0x10; // the cassette sense reads high
64
+ const p = this.port & 7;
65
+ if (a >= 0xe000) return p & 2 ? this.rom(this.kernal, a) : this.ram[a];
66
+ if (a >= 0xd000 && a < 0xe000) { if (p & 3 && p & 4) return this.io(a); if (p & 3) return this.chargen[a - 0xd000]; return this.ram[a]; }
67
+ if (a >= 0xa000 && a < 0xc000) return (p & 3) === 3 ? this.rom(this.basic, a) : this.ram[a];
68
+ return this.ram[a];
69
+ }
70
+ // a ROM's byte; in moonbase's stand-in only those it has (rom.js), another stops the machine
71
+ rom(r, a) {
72
+ const i = a - (r === this.kernal ? 0xe000 : 0xa000);
73
+ if (r.known && !r.known[i]) throw new Error(`the ${r === this.kernal ? 'KERNAL' : 'BASIC'} ROM at $${a.toString(16).toUpperCase()}: not handled`);
74
+ return r[i];
75
+ }
76
+ write(a, v) {
77
+ if (a === 0) { this.portDir = v; return; }
78
+ if (a === 1) { this.port = v; return; }
79
+ const p = this.port & 7;
80
+ if (a >= 0xd000 && a < 0xe000 && (p & 3) && (p & 4)) { this.ioWrite(a, v); return; }
81
+ this.ram[a] = v; // a write under a ROM goes to the RAM beneath
82
+ }
83
+
84
+ // the I/O page: the VIC-II at $D000, the SID at $D400, color RAM at $D800, the CIAs at $DC00 and $DD00
85
+ io(a) {
86
+ if (a < 0xd400) return this.vic.read(a & 0x3f);
87
+ if (a < 0xd800) return (a & 0x1f) === 0x1b ? Math.random() * 256 | 0 : (a & 0x1f) === 0x1c ? 0 : this.sid[a & 0x1f]; // OSC3 reads noise, ENV3 nothing
88
+ if (a < 0xdc00) return this.color[a - 0xd800] | 0xf0;
89
+ if (a < 0xdd00) return this.cia1.read(a & 0xf, this);
90
+ if (a < 0xde00) return this.cia2.read(a & 0xf, this);
91
+ return 0xff;
92
+ }
93
+ ioWrite(a, v) {
94
+ if (a < 0xd400) this.vic.write(a & 0x3f, v);
95
+ else if (a < 0xd800) { this.sid[a & 0x1f] = v; this.sidWrites.push([this.cpu.cycles, a & 0x1f, v]); }
96
+ else if (a < 0xdc00) this.color[a - 0xd800] = v & 15;
97
+ else if (a < 0xdd00) this.cia1.write(a & 0xf, v);
98
+ else if (a < 0xde00) this.cia2.write(a & 0xf, v);
99
+ }
100
+
101
+ // the keyboard matrix as CIA1 scans it: port A selects columns, low; port B reads rows, low
102
+ // for a key held; the joystick in port 2 pulls port A's low bits, in port 1 port B's
103
+ keyboardRows(columns) {
104
+ let rows = 0xff;
105
+ for (let c = 0; c < 8; c++) if (!(columns & (1 << c))) rows &= ~this.keys[c];
106
+ return rows & 0xff;
107
+ }
108
+
109
+ // the KERNAL's LOAD: the name at ($BB) of $B7 bytes, the device in $BA, the secondary address
110
+ // in $B9, and the address to load at in X and Y when the secondary address is 0. The file goes
111
+ // into memory, the end goes to $AE/$AF and X/Y, and carry clear says it worked.
112
+ load() {
113
+ const c = this.cpu, nameBytes = this.ram.subarray(this.ram[0xbb] | this.ram[0xbc] << 8, (this.ram[0xbb] | this.ram[0xbc] << 8) + this.ram[0xb7]), device = this.ram[0xba], sa = this.ram[0xb9];
114
+ const name = String.fromCharCode(...nameBytes);
115
+ if (device !== 8) { c.a = 5; c.c = 1; return; } // DEVICE NOT PRESENT
116
+ const raw = this.drive.load(nameBytes);
117
+ if (!raw || raw.length < 2 || c.a !== 0) { this.log('LOAD ' + JSON.stringify(name) + (raw ? ' verify' : ' not found')); c.a = 4; c.c = 1; this.ram[0x90] = 0x42; return; }
118
+ const at = sa === 0 ? c.x | c.y << 8 : raw[0] | raw[1] << 8, body = raw.subarray(2);
119
+ for (let i = 0; i < body.length; i++) this.write(at + i, body[i]);
120
+ const end = at + body.length;
121
+ this.ram[0xae] = end & 0xff; this.ram[0xaf] = end >> 8; this.ram[0x90] = 0;
122
+ c.x = end & 0xff; c.y = end >> 8; c.c = 0; c.a = 0;
123
+ this.log('LOAD', JSON.stringify(name), '$' + at.toString(16), body.length + ' bytes');
124
+ }
125
+ // SAVE: the start address is in the zero page at A, the end in X and Y
126
+ save() {
127
+ const c = this.cpu, name = this.pstringAt(this.ram[0xbb] | this.ram[0xbc] << 8, this.ram[0xb7]);
128
+ const from = this.ram[c.a] | this.ram[c.a + 1] << 8, to = c.x | c.y << 8;
129
+ const body = new Uint8Array(to - from + 2); body[0] = from & 0xff; body[1] = from >> 8;
130
+ for (let i = from; i < to; i++) body[i - from + 2] = this.read(i);
131
+ this.drive.save(this.ram.subarray(this.ram[0xbb] | this.ram[0xbc] << 8, (this.ram[0xbb] | this.ram[0xbc] << 8) + this.ram[0xb7]), body);
132
+ this.log('SAVE', JSON.stringify(name), body.length + ' bytes'); c.c = 0; c.a = 0;
133
+ }
134
+ pstringAt(a, n) { let s = ''; for (let i = 0; i < n; i++) s += String.fromCharCode(this.ram[a + i]); return s; }
135
+
136
+ // a key held or released, by its place in the matrix: column 0 to 7 and row 0 to 7
137
+ key(column, row, down) { if (down) this.keys[column] |= 1 << row; else this.keys[column] &= ~(1 << row); }
138
+ // type a line as if at the keyboard, through the KERNAL's buffer at $0277, once BASIC is ready
139
+ type(text) { const n = Math.min(10, text.length); for (let i = 0; i < n; i++) this.ram[0x277 + i] = text.charCodeAt(i); this.ram[0xc6] = n; return text.slice(n); }
140
+
141
+ // one raster line: the CPU's cycles, the CIAs' timers, the VIC-II's line, and any interrupt. The
142
+ // line is drawn with the registers and memory as they are where its display starts, cycle 15,
143
+ // as the VIC-II takes them: a register written later in the line (a demo's raster split, after
144
+ // its wait) shows from the next line, as the 64 shows the left of the screen (held to VICE on
145
+ // Subway Arts' $D016 at line $42)
146
+ runLine() {
147
+ const until = this.cpu.cycles + CYCLES_PER_LINE, display = this.cpu.cycles + DISPLAY_START;
148
+ let drawn = false;
149
+ while (this.cpu.cycles < until && !this.halted) {
150
+ if (!drawn && this.cpu.cycles >= display) { this.vic.line(this.line); drawn = true; }
151
+ const before = this.cpu.cycles;
152
+ const pc = this.cpu.pc;
153
+ if ((this.port & 2) === 0 && pc >= 0xe000 && !this.cpu.hooks.has(pc)) { /* RAM under the KERNAL: fine, it runs */ }
154
+ this.cpu.step();
155
+ const spent = this.cpu.cycles - before;
156
+ if (this.cia1.tick(spent) && (this.cia1.icr & 0x80)) this.cpu.irq();
157
+ // (NMI on its edge, as the 6502 takes it: once as CIA 2's line goes active, not again until
158
+ // it has gone inactive, its flags read, and active again; a demo that leaves it active,
159
+ // unacknowledged, is not interrupted for ever after, which is how RESTORE is locked out)
160
+ this.cia2.tick(spent);
161
+ const nmi = (this.cia2.icr & 0x80) !== 0;
162
+ if (nmi && !this.nmiLine) this.cpu.nmi();
163
+ this.nmiLine = nmi;
164
+ }
165
+ if (!drawn) this.vic.line(this.line);
166
+ if (this.vic.irq()) this.cpu.irq();
167
+ this.line = (this.line + 1) % LINES;
168
+ if (this.line === 0) this.frame++;
169
+ }
170
+ runFrame() { for (let i = 0; i < LINES; i++) this.runLine(); }
171
+ }
172
+
173
+ // A 6526 CIA: two timers counting cycles, an interrupt when one runs out, and two ports.
174
+ class CIA {
175
+ constructor() { this.ta = 0xffff; this.taLatch = 0xffff; this.tb = 0xffff; this.tbLatch = 0xffff; this.cra = 0; this.crb = 0; this.icr = 0; this.mask = 0; this.pra = 0xff; this.prb = 0xff; this.ddra = 0; this.ddrb = 0; this.tod = 0; }
176
+ read(r, m) {
177
+ switch (r) {
178
+ case 0: return m && this === m.cia1 ? (this.pra | ~this.ddra) & ~(m.joystick) & 0xff : (this.pra | ~this.ddra) & 0xff;
179
+ case 1: return m && this === m.cia1 ? m.keyboardRows(this.pra | ~this.ddra) & (this.prb | ~this.ddrb) : (this.prb | ~this.ddrb) & 0xff;
180
+ case 2: return this.ddra; case 3: return this.ddrb;
181
+ case 4: return this.ta & 0xff; case 5: return this.ta >> 8; case 6: return this.tb & 0xff; case 7: return this.tb >> 8;
182
+ case 8: return 0; case 9: return 0; case 10: return 0; case 11: return 0;
183
+ case 13: { const v = this.icr; this.icr = 0; return v; }
184
+ case 14: return this.cra; case 15: return this.crb;
185
+ }
186
+ return 0;
187
+ }
188
+ write(r, v) {
189
+ switch (r) {
190
+ case 0: this.pra = v; break; case 1: this.prb = v; break; case 2: this.ddra = v; break; case 3: this.ddrb = v; break;
191
+ case 4: this.taLatch = (this.taLatch & 0xff00) | v; break; case 5: this.taLatch = (this.taLatch & 0xff) | v << 8; if (!(this.cra & 1)) this.ta = this.taLatch; break;
192
+ case 6: this.tbLatch = (this.tbLatch & 0xff00) | v; break; case 7: this.tbLatch = (this.tbLatch & 0xff) | v << 8; if (!(this.crb & 1)) this.tb = this.tbLatch; break;
193
+ case 13: if (v & 0x80) this.mask |= v & 0x1f; else this.mask &= ~v; break;
194
+ case 14: this.cra = v; if (v & 0x10) this.ta = this.taLatch; break;
195
+ case 15: this.crb = v; if (v & 0x10) this.tb = this.tbLatch; break;
196
+ }
197
+ }
198
+ // the timers by `cycles`; true when an enabled interrupt is raised
199
+ tick(cycles) {
200
+ let raised = false;
201
+ if (this.cra & 1) {
202
+ this.ta -= cycles;
203
+ while (this.ta < 0) { this.ta += this.taLatch + 1; this.icr |= 1; if (this.cra & 8) { this.cra &= ~1; this.ta = this.taLatch; } if ((this.crb & 0x60) === 0x40 && this.crb & 1) { if (--this.tb < 0) { this.tb = this.tbLatch; this.icr |= 2; } } }
204
+ }
205
+ if (this.crb & 1 && (this.crb & 0x60) === 0) { this.tb -= cycles; while (this.tb < 0) { this.tb += this.tbLatch + 1; this.icr |= 2; if (this.crb & 8) { this.crb &= ~1; this.tb = this.tbLatch; } } }
206
+ if (this.icr & this.mask & 0x1f) { this.icr |= 0x80; raised = true; }
207
+ return raised;
208
+ }
209
+ }