@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/numbers.js ADDED
@@ -0,0 +1,64 @@
1
+ // Commodore BASIC's rules for numbers, held once for every layer that keeps them: the remake's
2
+ // BASIC (basic.js, on JavaScript's numbers) and moonbase's BASIC ROM (basic-rom.js, on the ROM's
3
+ // five-byte floats). BASIC's errors in its own words; a whole number of 16 bits; a number as FOUT
4
+ // writes it and as VAL reads it; and RND's numbers as moonbase gives them, the same on every line.
5
+ //
6
+ // (FOUT and VAL are held to the ROM's.)
7
+
8
+ import { Floats, CONSTANTS, pack } from './float.js';
9
+
10
+ // (an error's BASIC line, where it is not the statement's own, is its `basicLine`: WebKit gives
11
+ // every Error a `line` of its own, the line of the JavaScript it was thrown from)
12
+ export class BasicError extends Error {}
13
+ // BASIC's error messages, by their number in its table (the X its error routine takes, $A437, less 1)
14
+ export const ERRORS = ['TOO MANY FILES', 'FILE OPEN', 'FILE NOT OPEN', 'FILE NOT FOUND', 'DEVICE NOT PRESENT', 'NOT INPUT FILE', 'NOT OUTPUT FILE', 'MISSING FILE NAME',
15
+ 'ILLEGAL DEVICE NUMBER', 'NEXT WITHOUT FOR', 'SYNTAX', 'RETURN WITHOUT GOSUB', 'OUT OF DATA', 'ILLEGAL QUANTITY', 'OVERFLOW', 'OUT OF MEMORY', 'UNDEF\'D STATEMENT',
16
+ 'BAD SUBSCRIPT', 'REDIM\'D ARRAY', 'DIVISION BY ZERO', 'ILLEGAL DIRECT', 'TYPE MISMATCH', 'STRING TOO LONG', 'FILE DATA', 'FORMULA TOO COMPLEX', 'CAN\'T CONTINUE',
17
+ 'UNDEF\'D FUNCTION', 'VERIFY', 'LOAD', 'BREAK'];
18
+ export const fail = what => { throw new BasicError('?' + what + ' ERROR'); };
19
+
20
+ // A number as BASIC writes it (FOUT): a space for its sign when it is not negative, nine
21
+ // significant digits, no 0 before the point, and an exponent below 0.01 or from 1E+09 on. The
22
+ // number as BASIC's five bytes hold it, and FOUT's own steps on them (float.js, held to
23
+ // Microsoft's and Commodore's), its last digit as theirs: JavaScript's rounding of nine digits
24
+ // differs from it for one number in twenty.
25
+ const page = new Uint8Array(0x10000), floats = new Floats({ read: a => page[a], write: (a, v) => { page[a] = v; } });
26
+ for (const [at, bytes] of Object.entries(CONSTANTS)) page.set(bytes, +at);
27
+ export function fout(x) {
28
+ const b = pack(x);
29
+ if (!b[0] || Math.abs(x) < 2 ** -128) return ' 0';
30
+ if (b[0] > 255) fail('OVERFLOW');
31
+ page[0x61] = b[0]; page[0x62] = b[1] | 0x80; page.set(b.slice(2), 0x63); page[0x66] = x < 0 ? 0xff : 0; page[0x70] = 0;
32
+ floats.fout();
33
+ let s = ''; for (let a = 0x100; page[a]; a++) s += String.fromCharCode(page[a]);
34
+ return s;
35
+ }
36
+
37
+ // VAL: the number at a string's start, BASIC's way: spaces skipped, a sign, digits, a point, an
38
+ // exponent; 0 if there is none
39
+ export function val(s) {
40
+ const t = s.replace(/ /g, ''), m = /^[+-]?(\d*\.?\d*)(E[+-]?\d+)?/.exec(t);
41
+ const n = m && m[1] && m[1] !== '.' ? Number(m[0].replace(/E$/, '')) : 0;
42
+ return Number.isFinite(n) ? n : 0;
43
+ }
44
+
45
+ // A number's text where INPUT reads one, as FIN reads it ($BCF3): spaces passed over anywhere,
46
+ // as CHRGET passes them; a sign; digits; a point and digits; an E, a sign and digits. Where it
47
+ // ends, for INPUT to look at what follows (a comma, a colon or the line's end, or the item is bad)
48
+ export function scanNumber(s, k) {
49
+ let text = '';
50
+ const sp = () => { while (s[k] === ' ') k++; }, digits = () => { while (s[k] >= '0' && s[k] <= '9') { text += s[k++]; sp(); } };
51
+ sp();
52
+ if (s[k] === '-' || s[k] === '+') { text += s[k++]; sp(); }
53
+ digits();
54
+ if (s[k] === '.') { text += s[k++]; sp(); digits(); }
55
+ if (s[k] === 'E') { text += s[k++]; sp(); if (s[k] === '-' || s[k] === '+') { text += s[k++]; sp(); } digits(); }
56
+ return { text, end: k };
57
+ }
58
+
59
+ export const int16 = x => { const n = Math.floor(x); if (n < -32768 || n > 32767) fail('ILLEGAL QUANTITY'); return n; };
60
+
61
+ // the seed's random numbers: a 32-bit state, each number its top 31 bits as a fraction, which
62
+ // BASIC's five-byte floats hold exactly
63
+ export const seedRandom = seed => Math.imul(seed, 2654435761) >>> 0;
64
+ export const nextRandom = s => (((Math.imul(s, 1664525) + 1013904223) >>> 0) & 0xfffffffe) >>> 0;
package/package.json CHANGED
@@ -1,6 +1,16 @@
1
1
  {
2
2
  "name": "@moonbase-machines/c64",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.0",
4
+ "description": "A Commodore 64 as far as the games use it: the VIC-II, the SID, a 1541, a printer, and Moonbase's own KERNAL and BASIC in place of Commodore's ROMs.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Mark Wharton",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./*": "./*"
11
+ },
12
+ "dependencies": {
13
+ "@moonbase-machines/mos6502": "0.5.0",
14
+ "@moonbase-machines/charset": "0.5.0"
15
+ }
16
+ }
package/paint.js ADDED
@@ -0,0 +1,23 @@
1
+ // A Commodore 64's frame painted from memory and the chips' registers, by the VIC-II as the
2
+ // emulator draws it (vic.js), for a remake: its memory, its I/O page's bytes ($D000 to
3
+ // $DFFF as the remake keeps them: the VIC's registers, the colours, CIA 2's bank) and the
4
+ // character ROM, which a text screen in the ROM's own characters needs. And `splits`, where a
5
+ // game's raster interrupt changes the VIC's registers partway down (Find Me's two character
6
+ // sets): each line's interrupt's writes, from the line after, and the last's from the top.
7
+ import { VIC } from './vic.js';
8
+
9
+ export function paint(mem, chip, chargen, vic, splits) {
10
+ const m = { ram: mem, chargen: chargen ?? new Uint8Array(0x1000), color: chip.subarray(0x800, 0xc00), cia2: { pra: chip[0xd00] }, line: 0 };
11
+ vic ??= new VIC(m);
12
+ vic.m = m;
13
+ vic.r.set(chip.subarray(0, 0x40));
14
+ const at = splits?.size ? [...splits].sort((a, b) => a[0] - b[0]) : [];
15
+ // (a split's $100 is CIA 2's first port, the VIC's bank)
16
+ const set = (r, v) => { if (r === 0x100) m.cia2.pra = v; else vic.r[r] = v; };
17
+ if (at.length) for (const [r, v] of at.at(-1)[1]) set(r, v);
18
+ for (let y = 0, k = 0; y < 312; y++) {
19
+ while (k < at.length && at[k][0] < y) { for (const [r, v] of at[k][1]) set(r, v); k++; }
20
+ m.line = y; vic.line(y);
21
+ }
22
+ return vic.frame;
23
+ }
package/petscii.js ADDED
@@ -0,0 +1,54 @@
1
+ // PETSCII as text that can be typed and read, as moonbase's listings write it: the control
2
+ // characters by their names in braces, {CLR}, {RVS ON}, and any other byte as {$xx}; for game
3
+ // documents and a BASIC page.
4
+ //
5
+ // bytes('{CLR}HELLO', { lower }) the bytes a C64 prints: with lower, the second character
6
+ // set's (a small letter $41 to $5A, a capital $C1 to $DA);
7
+ // without, the first's (letters typed either way, $41 to $5A)
8
+ // text(bytes, { lower }) back again: with lower, the second character set's way
9
+ // ($41 to $5A small letters, $C1 to $DA capitals)
10
+
11
+ export const CONTROLS = { WHT: 0x05, 'DISABLE SHIFT': 0x08, 'ENABLE SHIFT': 0x09, RETURN: 0x0d, LOWER: 0x0e, DOWN: 0x11, 'RVS ON': 0x12, HOME: 0x13, DEL: 0x14,
12
+ RED: 0x1c, RIGHT: 0x1d, GRN: 0x1e, BLU: 0x1f, ORNG: 0x81, F1: 0x85, F3: 0x86, F5: 0x87, F7: 0x88, F2: 0x89, F4: 0x8a, F6: 0x8b, F8: 0x8c,
13
+ UPPER: 0x8e, BLK: 0x90, UP: 0x91, 'RVS OFF': 0x92, CLR: 0x93, INST: 0x94, BRN: 0x95,
14
+ LRED: 0x96, GRY1: 0x97, GRY2: 0x98, LGRN: 0x99, LBLU: 0x9a, GRY3: 0x9b, PUR: 0x9c, LEFT: 0x9d, YEL: 0x9e, CYN: 0x9f, 'SHIFT SPACE': 0xa0, PI: 0xff };
15
+ const NAMES = Object.fromEntries(Object.entries(CONTROLS).map(([n, b]) => [b, n]));
16
+
17
+ export function bytes(t, { lower = false } = {}) {
18
+ const out = [];
19
+ for (let k = 0; k < t.length; k++) {
20
+ const c = t[k];
21
+ if (c === '{') {
22
+ const end = t.indexOf('}', k), name = t.slice(k + 1, end);
23
+ if (end < 0) throw new Error('a { with no }: ' + t);
24
+ const n = /^\$([0-9A-F]{2})$/i.exec(name) ? parseInt(name.slice(1), 16) : CONTROLS[name.toUpperCase()];
25
+ if (n === undefined) throw new Error(`no control character {${name}}`);
26
+ out.push(n); k = end; continue;
27
+ }
28
+ const v = c.charCodeAt(0);
29
+ if (c === '£') out.push(0x5c);
30
+ else if (c === '↑') out.push(0x5e);
31
+ else if (c === '←') out.push(0x5f);
32
+ else if (c === 'π') out.push(0xff);
33
+ else if (v >= 0x61 && v <= 0x7a) out.push(v - 0x20);
34
+ else if (lower && v >= 0x41 && v <= 0x5a) out.push(v + 0x80);
35
+ else out.push(v & 0xff);
36
+ }
37
+ return out;
38
+ }
39
+
40
+ export function text(b, { lower = false } = {}) {
41
+ let out = '';
42
+ for (const v of b) {
43
+ if (lower && v >= 0x41 && v <= 0x5a) out += String.fromCharCode(v + 0x20);
44
+ else if (lower && v >= 0xc1 && v <= 0xda) out += String.fromCharCode(v - 0x80);
45
+ else if (v === 0x0d) out += '\r';
46
+ else if (v >= 0x20 && v < 0x5b || v === 0x5d) out += String.fromCharCode(v);
47
+ else if (v === 0x5c) out += '£';
48
+ else if (v === 0x5e) out += '↑';
49
+ else if (v === 0x5f) out += '←';
50
+ else if (v === 0x5b) out += '[';
51
+ else out += `{${NAMES[v] ?? '$' + v.toString(16).toUpperCase().padStart(2, '0')}}`;
52
+ }
53
+ return out;
54
+ }
package/printer.js ADDED
@@ -0,0 +1,95 @@
1
+ // A printer on the Commodore 64's serial bus, device 4, as the games print to it: OPEN 4,4 (or
2
+ // 4,4,7 for small letters), PRINT# and CLOSE. It keeps what it is sent as lines of characters, in
3
+ // the mode it was in for each (capitals and graphics, or small letters and capitals), for a page
4
+ // to show as a sheet of paper (`paper`). It is not on the bus unless it is switched on, as the
5
+ // machines were sold: with it off, a game finds DEVICE NOT PRESENT, as Goldfields does.
6
+ //
7
+ // The bus (`bus`) puts it beside the drive: LISTEN, SECOND, CIOUT and UNLISTEN go to every device,
8
+ // and each takes them only while it is the one listening; TALK and the rest, and LOAD and SAVE,
9
+ // are the drive's alone.
10
+ import { screenCode } from './kernal.js';
11
+
12
+ export const WIDTH = 80; // the characters a line holds, as Commodore's printers had
13
+
14
+ export class Printer {
15
+ constructor({ on = true } = {}) {
16
+ this.on = on;
17
+ this.listening = false; this.lower = false; this.wide = false; this.reverse = false;
18
+ this.lines = []; this.line = [];
19
+ this.changed = () => {}; // called when a line is finished, for a page to draw it
20
+ }
21
+ listen(device) { this.listening = this.on && device === 4; return this.listening; }
22
+ talk() { return false; }
23
+ // the secondary address: 7 is small letters and capitals, the rest capitals and graphics
24
+ second(sa) { if (this.listening) this.lower = (sa & 15) === 7; }
25
+ tksa() {}
26
+ ciout(b) {
27
+ if (!this.listening) return;
28
+ switch (b) {
29
+ case 13: this.feed(); return; // a new line
30
+ case 10: return;
31
+ case 17: this.lower = true; return; // small letters and capitals
32
+ case 145: this.lower = false; return; // capitals and graphics
33
+ case 14: this.wide = true; return; // double width
34
+ case 15: this.wide = false; return;
35
+ case 18: this.reverse = true; return; // reversed
36
+ case 146: this.reverse = false; return;
37
+ case 29: b = 32; break; // (cursor right: a space)
38
+ }
39
+ if ((b & 0x7f) < 32) return; // the other controls
40
+ if (this.line.length >= WIDTH) this.feed();
41
+ this.line.push({ code: b, lower: this.lower, wide: this.wide, reverse: this.reverse });
42
+ }
43
+ feed() { this.lines.push(this.line); this.line = []; this.wide = false; this.reverse = false; this.changed(); }
44
+ unlsn() { this.listening = false; }
45
+ untlk() {}
46
+ acptr() { return null; }
47
+ tear() { this.lines = []; this.line = []; this.changed(); } // the paper torn off
48
+ }
49
+
50
+ // the drive and the printer on one bus, as the KERNAL's serial routines reach them
51
+ export function bus(drive, printer) {
52
+ const all = [drive, printer];
53
+ return {
54
+ drive, printer,
55
+ listen: d => all.map(x => x.listen(d)).some(Boolean),
56
+ talk: d => drive.talk(d),
57
+ second: sa => all.forEach(x => x.second(sa)),
58
+ tksa: sa => drive.tksa(sa),
59
+ ciout: b => all.forEach(x => x.ciout(b)),
60
+ unlsn: () => all.forEach(x => x.unlsn()),
61
+ untlk: () => drive.untlk(),
62
+ acptr: () => drive.acptr(),
63
+ load: name => drive.load(name),
64
+ save: (name, raw) => drive.save(name, raw),
65
+ find: name => drive.find(name),
66
+ };
67
+ }
68
+
69
+ // The sheet: what the printer has printed, drawn on a canvas in the 64's own characters (the
70
+ // character ROM, `chargen`), black on continuous paper with its feed holes down both sides
71
+ export function paper(canvas, printer, chargen, { scale = 2 } = {}) {
72
+ // (a line a character and a half high, as a printer spaces them)
73
+ const margin = 24, cell = 8 * scale, line = cell * 1.5, rows = Math.max(12, printer.lines.length + 2);
74
+ canvas.width = (WIDTH * 8 + margin * 2) * scale / 2 * 2; canvas.height = rows * line + cell;
75
+ const g = canvas.getContext('2d'), w = canvas.width;
76
+ g.fillStyle = '#fbfaf3'; g.fillRect(0, 0, w, canvas.height);
77
+ for (let r = 0; r < rows; r += 6) { g.fillStyle = '#e6f0e2'; g.fillRect(margin * scale / 2, r * line + cell / 2, w - margin * scale, line * 3); } // the green bars
78
+ g.fillStyle = '#d9d6c8';
79
+ for (let y = cell / 2; y < canvas.height; y += cell * 2) for (const x of [margin * scale / 4, w - margin * scale / 4]) { g.beginPath(); g.arc(x, y, scale * 2.5, 0, Math.PI * 2); g.fill(); }
80
+ const lines = [...printer.lines, printer.line], dot = scale;
81
+ lines.forEach((chars, r) => {
82
+ let x = margin * scale / 2 + cell / 2;
83
+ for (const c of chars) {
84
+ const sc = screenCode(c.code), base = (c.lower ? 0x800 : 0) + sc * 8, wide = c.wide ? 2 : 1;
85
+ for (let py = 0; py < 8; py++) {
86
+ const bits = chargen ? chargen[base + py] : 0;
87
+ for (let px = 0; px < 8; px++) {
88
+ const on = (bits >> (7 - px) & 1) ^ (c.reverse ? 1 : 0);
89
+ if (on) { g.fillStyle = '#26241f'; g.fillRect(x + px * dot * wide, r * line + cell / 2 + py * dot, dot * wide, dot); }
90
+ }
91
+ }
92
+ x += cell * wide;
93
+ }
94
+ });
95
+ }