@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/LICENSE +21 -0
- package/README.md +11 -2
- package/basic-compile.js +171 -0
- package/basic-lang.js +394 -0
- package/basic-list.js +21 -0
- package/basic-rom.js +572 -0
- package/basic.js +275 -0
- package/browser.js +116 -0
- package/d64.js +191 -0
- package/dist/moonbase-c64.js +9 -0
- package/dist/moonbase-c64.js.map +7 -0
- package/drive.js +209 -0
- package/editor.js +267 -0
- package/float.js +584 -0
- package/glyph-hash.js +8 -0
- package/glyph-hashes.js +70 -0
- package/index.js +27 -0
- package/kernal.js +319 -0
- package/keys.js +21 -0
- package/machine.js +209 -0
- package/numbers.js +64 -0
- package/package.json +14 -4
- package/paint.js +23 -0
- package/petscii.js +54 -0
- package/printer.js +95 -0
- package/rom.js +537 -0
- package/saves.js +10 -0
- package/sid.js +49 -0
- package/start.js +33 -0
- package/test/disks.test.js +35 -0
- package/vic.js +122 -0
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
|
+
}
|