@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/drive.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// A Commodore 1541 as far as the games use it: the disk image itself, read a sector at a time;
|
|
2
|
+
// the directory, searched with the drive's own wildcards; the command channel, with the memory
|
|
3
|
+
// commands M-W, M-R and M-E, the block read U1 and the buffer pointer B-P; LOAD"$", the
|
|
4
|
+
// directory as a BASIC program; files opened by name
|
|
5
|
+
// and read a byte at a time; and the drive's memory, since Zoopak reads the directory through it
|
|
6
|
+
// and then pokes a picture's track and sector into the two places the drive keeps its last
|
|
7
|
+
// file's, so that LOAD"*", which on a 1541 loads the last file without a directory search,
|
|
8
|
+
// fetches that picture. The KERNAL's bus routines, LISTEN, SECOND, CIOUT, UNLSN, TALK, TKSA,
|
|
9
|
+
// ACPTR and UNTLK, are answered here by the machine's hooks, so the KERNAL's own OPEN, CHRIN
|
|
10
|
+
// and CLOSE work on device 8 without the serial bus being modelled.
|
|
11
|
+
|
|
12
|
+
import { add } from './d64.js';
|
|
13
|
+
|
|
14
|
+
const SECTORS = [...Array(17).fill(21), ...Array(7).fill(19), ...Array(6).fill(18), ...Array(5).fill(17)];
|
|
15
|
+
const TYPES = ['DEL', 'SEQ', 'PRG', 'USR', 'REL'];
|
|
16
|
+
const OK = '00, OK,00,00\r', NOT_FOUND = '62,FILE NOT FOUND,00,00\r', SYNTAX = '31,SYNTAX ERROR,00,00\r';
|
|
17
|
+
const bytes = s => Uint8Array.from(s, c => c.charCodeAt(0));
|
|
18
|
+
const upper = b => (b >= 0xc1 && b <= 0xda ? b - 0x80 : b); // shifted letters as unshifted
|
|
19
|
+
|
|
20
|
+
export class Drive {
|
|
21
|
+
constructor(image) {
|
|
22
|
+
this.image = image;
|
|
23
|
+
this.mem = new Uint8Array(0x800); // the drive's RAM: $0200 the command buffer, $0300 to $07FF the four buffers
|
|
24
|
+
this.saved = new Map(); // files saved this session, by name, as raw bytes; they shadow the disk
|
|
25
|
+
this.keep = null; // (name, raw): told of each file saved, to keep it (saves.js)
|
|
26
|
+
this.channels = new Map(); // open channels by secondary address 0 to 15
|
|
27
|
+
this.listening = false; this.talking = false; this.channel = 0; this.opening = null;
|
|
28
|
+
this.status = bytes(OK); this.reply = null; this.pos = 0;
|
|
29
|
+
this.channels.set(15, { command: [] });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// the disk
|
|
33
|
+
sector(t, s, image = this.image) { const n = SECTORS.slice(0, t - 1).reduce((a, b) => a + b, 0) + s; return image.subarray(n * 256, (n + 1) * 256); }
|
|
34
|
+
chain(t, s) {
|
|
35
|
+
const parts = []; let n = 0;
|
|
36
|
+
while (t && n++ < 800) { const b = this.sector(t, s); [t, s] = [b[0], b[1]]; parts.push(b.subarray(2, t ? 256 : s + 1)); }
|
|
37
|
+
const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0)); let at = 0;
|
|
38
|
+
for (const p of parts) { out.set(p, at); at += p.length; }
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
directory() {
|
|
42
|
+
const bam = this.sector(18, 0), files = [];
|
|
43
|
+
for (let [t, s] = [bam[0], bam[1]], n = 0; t && n++ < 40; ) {
|
|
44
|
+
const b = this.sector(t, s);
|
|
45
|
+
for (let i = 0; i < 8; i++) {
|
|
46
|
+
const e = b.subarray(i * 32, (i + 1) * 32);
|
|
47
|
+
if (!e[2]) continue;
|
|
48
|
+
files.push({ name: Array.from(e.subarray(5, 21)).filter(c => c !== 0xa0).map(upper), type: TYPES[e[2] & 7] || '?', track: e[3], sector: e[4] });
|
|
49
|
+
}
|
|
50
|
+
[t, s] = [b[0], b[1]];
|
|
51
|
+
}
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
// the files a name can find: not DEL's, which the 1541's search passes over (a directory's art,
|
|
55
|
+
// drawn in DEL entries' names, so that none of them loads)
|
|
56
|
+
loadable() { return this.directory().filter(f => f.type !== 'DEL'); }
|
|
57
|
+
// a name as the drive matches it: the drive prefix dropped, '*' ending the comparison, '?' any character
|
|
58
|
+
static matches(pattern, name) {
|
|
59
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
60
|
+
if (pattern[i] === 0x2a) return true;
|
|
61
|
+
if (i >= name.length) return false;
|
|
62
|
+
if (pattern[i] !== 0x3f && upper(pattern[i]) !== name[i]) return false;
|
|
63
|
+
}
|
|
64
|
+
return pattern.length === name.length;
|
|
65
|
+
}
|
|
66
|
+
static strip(name) { let n = Array.from(name).map(upper); if (n[0] === 0x40) n = n.slice(1); if (n[1] === 0x3a) n = n.slice(2); return n; }
|
|
67
|
+
find(name) {
|
|
68
|
+
name = Drive.strip(name);
|
|
69
|
+
for (const [k, raw] of this.saved) if (Drive.matches(name, Array.from(bytes(k)))) return { raw, saved: true };
|
|
70
|
+
for (const f of this.loadable()) if (Drive.matches(name, f.name)) return { ...f, raw: this.chain(f.track, f.sector) };
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
// LOAD"$": the directory as the 1541 makes it, a BASIC program at $0401: the header line, the
|
|
74
|
+
// disk's name reversed in quotes with its ID and DOS type; a line for each entry, its blocks the
|
|
75
|
+
// line's number, its name in quotes as it is, its type (* if not closed, < if locked); and the
|
|
76
|
+
// blocks free. BASIC's LOAD links its lines after. The files saved this session are listed as a
|
|
77
|
+
// 1541 would have them, the disk read as if they were on it (d64.js `add`)
|
|
78
|
+
listing() {
|
|
79
|
+
const image = this.saved.size ? add(this.image, [...this.saved].map(([name, data]) => ({ name, data }))) : this.image;
|
|
80
|
+
const sector = (t, s) => this.sector(t, s, image);
|
|
81
|
+
const bam = sector(18, 0), out = [0x01, 0x04];
|
|
82
|
+
const line = (n, text) => out.push(0x01, 0x01, n & 255, n >> 8, ...text, 0);
|
|
83
|
+
line(0, [0x12, 0x22, ...bam.subarray(0x90, 0xa0), 0x22, 0x20, bam[0xa2], bam[0xa3], 0x20, bam[0xa5], bam[0xa6]]);
|
|
84
|
+
for (let [t, s] = [bam[0], bam[1]], k = 0; t && k++ < 40; ) {
|
|
85
|
+
const b = sector(t, s);
|
|
86
|
+
for (let i = 0; i < 8; i++) {
|
|
87
|
+
const e = b.subarray(i * 32, (i + 1) * 32);
|
|
88
|
+
if (!e[2]) continue;
|
|
89
|
+
const blocks = e[30] | e[31] << 8, raw = Array.from(e.subarray(5, 21)), end = raw.indexOf(0xa0), name = end < 0 ? raw : raw.slice(0, end);
|
|
90
|
+
const pad = blocks < 10 ? 3 : blocks < 100 ? 2 : 1;
|
|
91
|
+
line(blocks, [...Array(pad).fill(0x20), 0x22, ...name, 0x22, ...Array(16 - name.length).fill(0x20), e[2] & 0x80 ? 0x20 : 0x2a, ...bytes(TYPES[e[2] & 7] || '???'), e[2] & 0x40 ? 0x3c : 0x20]);
|
|
92
|
+
}
|
|
93
|
+
[t, s] = [b[0], b[1]];
|
|
94
|
+
}
|
|
95
|
+
let free = 0; for (let t = 1; t <= 35; t++) if (t !== 18) free += bam[4 * t];
|
|
96
|
+
line(free, [...bytes('BLOCKS FREE.'), ...Array(13).fill(0x20)]);
|
|
97
|
+
out.push(0, 0);
|
|
98
|
+
return Uint8Array.from(out);
|
|
99
|
+
}
|
|
100
|
+
// the drive keeps its last file's track and sector at $7E and $026F, and LOAD"*" starts there
|
|
101
|
+
remember(f) { if (f && f.track) { this.mem[0x7e] = f.track; this.mem[0x26e] = 0; this.mem[0x26f] = f.sector; } }
|
|
102
|
+
load(name) {
|
|
103
|
+
name = Drive.strip(name);
|
|
104
|
+
let f;
|
|
105
|
+
if (name[0] === 0x24) { this.status = bytes(OK); return this.listing(); }
|
|
106
|
+
if (name.length === 1 && name[0] === 0x2a) f = this.mem[0x7e] ? { raw: this.chain(this.mem[0x7e], this.mem[0x26f]) } : { ...this.loadable()[0], raw: null };
|
|
107
|
+
else f = this.find(name);
|
|
108
|
+
if (f && !f.raw) f.raw = this.chain(f.track, f.sector);
|
|
109
|
+
this.remember(f); this.status = bytes(f ? OK : NOT_FOUND);
|
|
110
|
+
return f && f.raw;
|
|
111
|
+
}
|
|
112
|
+
save(name, raw) { this.store(String.fromCharCode(...Drive.strip(name)), raw); this.status = bytes(OK); }
|
|
113
|
+
store(name, raw) { this.saved.set(name, raw); this.keep?.(name, raw); }
|
|
114
|
+
|
|
115
|
+
// the bus, as the KERNAL's routines present it
|
|
116
|
+
listen(device) { this.listening = device === 8; return this.listening; }
|
|
117
|
+
talk(device) { this.talking = device === 8; return this.talking; }
|
|
118
|
+
second(sa) {
|
|
119
|
+
if (!this.listening) return;
|
|
120
|
+
const ch = sa & 15, kind = sa & 0xf0;
|
|
121
|
+
if (kind === 0xf0) this.opening = { ch, name: [] };
|
|
122
|
+
else if (kind === 0xe0) this.close(ch);
|
|
123
|
+
else this.channel = ch;
|
|
124
|
+
}
|
|
125
|
+
tksa(sa) { if (this.talking) { this.channel = sa & 15; this.pos = 0; } }
|
|
126
|
+
ciout(b) {
|
|
127
|
+
if (!this.listening) return;
|
|
128
|
+
if (this.opening) { this.opening.name.push(b); return; }
|
|
129
|
+
const c = this.channels.get(this.channel);
|
|
130
|
+
if (!c) return;
|
|
131
|
+
if (c.command) c.command.push(b);
|
|
132
|
+
else if (c.write) c.write.push(b);
|
|
133
|
+
else if (c.buffer) { c.buffer[c.pos] = b; c.pos = (c.pos + 1) & 0xff; }
|
|
134
|
+
}
|
|
135
|
+
unlsn() {
|
|
136
|
+
if (this.opening) { this.open(this.opening.ch, this.opening.name); this.opening = null; }
|
|
137
|
+
else if (this.channel === 15) { const c = this.channels.get(15); if (c.command.length) { this.execute(Uint8Array.from(c.command)); c.command = []; } }
|
|
138
|
+
this.listening = false;
|
|
139
|
+
}
|
|
140
|
+
untlk() { this.talking = false; }
|
|
141
|
+
// the next byte for the talker, and whether it is the last; nothing when the channel is empty
|
|
142
|
+
acptr() {
|
|
143
|
+
const c = this.channels.get(this.channel);
|
|
144
|
+
if (this.channel === 15) {
|
|
145
|
+
const data = this.reply || this.status;
|
|
146
|
+
if (this.pos >= data.length) { this.reply = null; this.pos = 0; return null; }
|
|
147
|
+
const byte = data[this.pos++], eoi = this.pos >= data.length;
|
|
148
|
+
if (eoi) { if (this.reply) this.reply = null; else this.status = bytes(OK); this.pos = 0; }
|
|
149
|
+
return { byte, eoi };
|
|
150
|
+
}
|
|
151
|
+
if (!c) return null;
|
|
152
|
+
if (c.buffer) { const byte = c.buffer[c.pos]; c.pos = (c.pos + 1) & 0xff; return { byte, eoi: c.pos === 0 }; }
|
|
153
|
+
if (!c.data || c.pos >= c.data.length) return null;
|
|
154
|
+
return { byte: c.data[c.pos++], eoi: c.pos >= c.data.length };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// a file opened on a channel: '#' a buffer, else a name with its type and mode after commas
|
|
158
|
+
open(ch, name) {
|
|
159
|
+
if (ch === 15) { if (name.length) this.execute(Uint8Array.from(name)); return; }
|
|
160
|
+
if (name[0] === 0x23) { this.channels.set(ch, { buffer: new Uint8Array(256), pos: 0 }); this.status = bytes(OK); return; }
|
|
161
|
+
const parts = []; let cur = [];
|
|
162
|
+
for (const b of name) { if (b === 0x2c) { parts.push(cur); cur = []; } else cur.push(b); }
|
|
163
|
+
parts.push(cur);
|
|
164
|
+
// ,W writes; and channel 1 always, SAVE's, as channel 0 always reads, LOAD's
|
|
165
|
+
const mode = ch === 1 || ch !== 0 && parts.slice(1).some(p => p[0] === 0x57) ? 'W' : 'R';
|
|
166
|
+
if (mode === 'W') { this.channels.set(ch, { write: [], name: String.fromCharCode(...Drive.strip(parts[0])) }); this.status = bytes(OK); return; }
|
|
167
|
+
// (a file to read as LOAD finds it: "*" the last file, as a 1541 has it)
|
|
168
|
+
const raw = this.load(parts[0]);
|
|
169
|
+
this.channels.set(ch, { data: raw ?? new Uint8Array(0), pos: 0 });
|
|
170
|
+
}
|
|
171
|
+
close(ch) {
|
|
172
|
+
const c = this.channels.get(ch);
|
|
173
|
+
if (c && c.write) this.store(c.name, Uint8Array.from(c.write));
|
|
174
|
+
if (ch !== 15) this.channels.delete(ch);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// a command on channel 15
|
|
178
|
+
execute(cmd) {
|
|
179
|
+
this.mem.set(cmd.subarray(0, 0x2a), 0x200); // the command buffer, where M-E code runs from
|
|
180
|
+
const text = String.fromCharCode(...cmd);
|
|
181
|
+
this.status = bytes(OK); this.reply = null;
|
|
182
|
+
if (text.startsWith('M-W')) { const at = cmd[3] | cmd[4] << 8, n = cmd[5]; for (let i = 0; i < n && 6 + i < cmd.length; i++) this.mem[(at + i) & 0x7ff] = cmd[6 + i]; return; }
|
|
183
|
+
if (text.startsWith('M-R')) { const at = cmd[3] | cmd[4] << 8, n = cmd.length > 5 ? cmd[5] || 256 : 1; this.reply = this.mem.slice(at & 0x7ff, (at & 0x7ff) + n); this.pos = 0; return; }
|
|
184
|
+
if (text.startsWith('M-E')) {
|
|
185
|
+
// the one program the games send: LDX #track, LDY #sector, STX $06, STY $07, LDA #$80 (read),
|
|
186
|
+
// STA $00, and wait, which reads a sector into the drive's buffer 0 at $0300
|
|
187
|
+
const code = cmd.subarray(5);
|
|
188
|
+
for (let i = 0; i + 8 <= code.length; i++)
|
|
189
|
+
if (code[i] === 0xa2 && code[i + 2] === 0xa0 && code[i + 4] === 0x86 && code[i + 5] === 0x06 && code[i + 6] === 0x84 && code[i + 7] === 0x07) {
|
|
190
|
+
const t = code[i + 1], s = code[i + 3];
|
|
191
|
+
if (t >= 1 && t <= 35 && s < SECTORS[t - 1]) this.mem.set(this.sector(t, s), 0x300);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const m = /^(U1|UA|B-R|B-P)[: ]?\s*(.*)$/.exec(text.replace(/\r$/, ''));
|
|
197
|
+
if (m) {
|
|
198
|
+
const n = m[2].split(/[\s,]+/).filter(Boolean).map(Number), c = this.channels.get(n[0]);
|
|
199
|
+
if (!c || !c.buffer) { this.status = bytes('70,NO CHANNEL,00,00\r'); return; }
|
|
200
|
+
if (m[1] === 'B-P') { c.pos = n[1] & 0xff; return; }
|
|
201
|
+
const [, , t, s] = n;
|
|
202
|
+
if (t >= 1 && t <= 35 && s < SECTORS[t - 1]) { c.buffer.set(this.sector(t, s)); c.pos = 0; }
|
|
203
|
+
else this.status = bytes('66,ILLEGAL TRACK OR SECTOR,' + String(t).padStart(2, '0') + ',' + String(s).padStart(2, '0') + '\r');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (/^I/.test(text)) return;
|
|
207
|
+
this.status = bytes(SYNTAX);
|
|
208
|
+
}
|
|
209
|
+
}
|
package/editor.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// The KERNAL's screen editor, moonbase's own: what CHROUT does with a character for the screen,
|
|
2
|
+
// as the Commodore 64's KERNAL does it, keeping its working storage as it keeps it, so that a game
|
|
3
|
+
// that reads it (Goldfields' TEXT/CODE reads the table of lines) finds what it would. Shared by
|
|
4
|
+
// moonbase's KERNAL (kernal.js), for the remake and the original alike.
|
|
5
|
+
//
|
|
6
|
+
// const ed = editor({ read, write, keyRow })
|
|
7
|
+
// ed.chrout(ch) a character to the screen ($E716)
|
|
8
|
+
// ed.clear() the screen cleared, its lines made single ($E544); ed.home() ($E566)
|
|
9
|
+
// ed.setLine() the pointers set from the row and the column ($E56C), as PLOT ends
|
|
10
|
+
// ed.clearLine(x) a row cleared ($E9FF)
|
|
11
|
+
// ed.begin(), ed.input() the keyboard's line, as CHRIN reads it ($F157, $E632): a character,
|
|
12
|
+
// or null while it waits for a key
|
|
13
|
+
//
|
|
14
|
+
// The screen is lines of 40 columns, and a logical line one or two of them. Its storage:
|
|
15
|
+
// $D1/$D2 where the cursor's logical line starts on the screen; $F3/$F4 the same in colour
|
|
16
|
+
// $D3 the cursor's column in its logical line (0 to 79); $D5 its last column (39 or 79)
|
|
17
|
+
// $D6 the cursor's row; $D9 to $F2 a byte a row, the high byte of the row's place (its low
|
|
18
|
+
// two bits, with $0288 the page) and its top bit set if the row starts a logical line
|
|
19
|
+
// $C7 reverse on; $D4 quote mode; $D8 inserts to come; $0286 the colour; $CD the blink's count
|
|
20
|
+
// A row's low byte of place is 40 times it, as the KERNAL's table at $ECF0 has it.
|
|
21
|
+
const COLOURS = [0x90, 0x05, 0x1c, 0x9f, 0x9c, 0x1e, 0x1f, 0x9e, 0x81, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b];
|
|
22
|
+
|
|
23
|
+
export function editor({ read: r, write: w, keyRow = () => 0xff }) {
|
|
24
|
+
const W = (a, v) => w(a, v & 0xff);
|
|
25
|
+
const low = x => (x * 40) & 0xff;
|
|
26
|
+
// ($E9F0) the line pointer for row X: its low byte, and its high from the table and $0288
|
|
27
|
+
const linePointer = x => { W(0xd1, low(x)); W(0xd2, (r(0xd9 + x) & 3) | r(0x288)); };
|
|
28
|
+
// ($EA24) the colour pointer from the line pointer
|
|
29
|
+
const colourPointer = () => { W(0xf3, r(0xd1)); W(0xf4, (r(0xd2) & 3) | 0xd8); };
|
|
30
|
+
const pnt = () => r(0xd1) | r(0xd2) << 8, user = () => r(0xf3) | r(0xf4) << 8;
|
|
31
|
+
// ($E9FF) row X cleared to spaces, in the cursor's colour ($0286, as $E4DA puts it)
|
|
32
|
+
function clearLine(x) {
|
|
33
|
+
linePointer(x); colourPointer();
|
|
34
|
+
for (let y = 39; y >= 0; y--) { w(user() + y, r(0x286)); w(pnt() + y, 0x20); }
|
|
35
|
+
}
|
|
36
|
+
// ($E56C) the pointers from the row and the column: back to the logical line's first row, the
|
|
37
|
+
// column counted on from it; the line's last column from the rows that go on from it
|
|
38
|
+
function setLine() {
|
|
39
|
+
let x = r(0xd6), a = r(0xd3);
|
|
40
|
+
while (!(r(0xd9 + x) & 0x80)) { a = (a + 40) & 0xff; W(0xd3, a); x--; if (x < 0) break; }
|
|
41
|
+
linePointer(x & 0xff);
|
|
42
|
+
let max = 0x27; x++;
|
|
43
|
+
while (!(r(0xd9 + x) & 0x80)) { max += 40; x++; if (x > 0x7f) break; }
|
|
44
|
+
W(0xd5, max);
|
|
45
|
+
colourPointer();
|
|
46
|
+
}
|
|
47
|
+
// ($E544) the screen cleared: every row a logical line of its own, the table's end marked, the
|
|
48
|
+
// rows cleared from the bottom; then home ($E566)
|
|
49
|
+
function clear() {
|
|
50
|
+
let y = r(0x288) | 0x80, a = 0;
|
|
51
|
+
for (let x = 0; x < 0x1a; x++) { W(0xd9 + x, y); a += 40; if (a > 255) { a &= 0xff; y++; } }
|
|
52
|
+
W(0xd9 + 0x1a, 0xff);
|
|
53
|
+
for (let x = 0x18; x >= 0; x--) clearLine(x);
|
|
54
|
+
home();
|
|
55
|
+
}
|
|
56
|
+
function home() { W(0xd3, 0); W(0xd6, 0); setLine(); }
|
|
57
|
+
// ($E8EA) the screen scrolled up a row: each row moved up from the one below, the table's
|
|
58
|
+
// links moved with them, the last row cleared; again while the first row goes on from above;
|
|
59
|
+
// and with CTRL held a moment's wait. (The cursor's row, the input's row and $02A5 a row up.)
|
|
60
|
+
function scroll() {
|
|
61
|
+
const kept = [0xac, 0xad, 0xae, 0xaf].map(r);
|
|
62
|
+
for (;;) {
|
|
63
|
+
W(0xd6, r(0xd6) - 1); W(0xc9, r(0xc9) - 1); W(0x2a5, r(0x2a5) - 1);
|
|
64
|
+
let x;
|
|
65
|
+
for (x = 0; x < 0x18; x++) { linePointer(x); moveLine(low(x + 1), r(0xda + x)); }
|
|
66
|
+
linePointer(x);
|
|
67
|
+
clearLine(0x18);
|
|
68
|
+
for (let k = 0; k < 0x18; k++) W(0xd9 + k, (r(0xd9 + k) & 0x7f) | (r(0xda + k) & 0x80));
|
|
69
|
+
W(0xf1, r(0xf1) | 0x80);
|
|
70
|
+
if (r(0xd9) & 0x80) break;
|
|
71
|
+
}
|
|
72
|
+
W(0xd6, r(0xd6) + 1); W(0x2a5, r(0x2a5) + 1);
|
|
73
|
+
if (keyRow(0x7f) === 0xfb) W(0xc6, 0); // CTRL held: the KERNAL waits a moment, and empties the keyboard's buffer
|
|
74
|
+
[0xac, 0xad, 0xae, 0xaf].forEach((a, k) => W(a, kept[k]));
|
|
75
|
+
}
|
|
76
|
+
// ($E9C8) a row copied to the line pointer's, from the row whose low byte is `from` and whose
|
|
77
|
+
// table entry is `entry`
|
|
78
|
+
function moveLine(from, entry) {
|
|
79
|
+
const src = from | ((entry & 3) | r(0x288)) << 8, srcColour = from | ((entry & 3) | 0xd8) << 8;
|
|
80
|
+
W(0xac, from); W(0xad, src >> 8); colourPointer(); W(0xae, from); W(0xaf, srcColour >> 8);
|
|
81
|
+
for (let y = 0x27; y >= 0; y--) { w(pnt() + y, r(src + y)); w(user() + y, r(srcColour + y)); }
|
|
82
|
+
}
|
|
83
|
+
// ($E965) a row opened below the logical line, for it to grow into: the rows below moved down,
|
|
84
|
+
// or the screen scrolled first if the line is at the bottom
|
|
85
|
+
function openLine(from = r(0xd6) + 1) {
|
|
86
|
+
let x = from;
|
|
87
|
+
while (!(r(0xd9 + x) & 0x80)) x++;
|
|
88
|
+
W(0x2a5, x);
|
|
89
|
+
if (x > 0x18) { scroll(); x = r(0x2a5) - 1; W(0xd6, r(0xd6) - 1); return grow(x); }
|
|
90
|
+
const kept = [0xac, 0xad, 0xae, 0xaf].map(r);
|
|
91
|
+
for (let k = 0x18; k > r(0x2a5); k--) { linePointer(k); moveLine(low(k - 1), r(0xd8 + k)); }
|
|
92
|
+
linePointer(Math.max(r(0x2a5), 0));
|
|
93
|
+
clearLine(r(0x2a5));
|
|
94
|
+
for (let k = 0x17; k >= r(0x2a5) && k > 0; k--) W(0xda + k, (r(0xda + k) & 0x7f) | (r(0xd9 + k) & 0x80));
|
|
95
|
+
grow(r(0x2a5));
|
|
96
|
+
[0xac, 0xad, 0xae, 0xaf].forEach((a, k) => W(a, kept[k]));
|
|
97
|
+
}
|
|
98
|
+
// ($E6DA) the logical line made two rows: the cursor's row's link cleared, the next row's
|
|
99
|
+
// marked its own start, the line's last column 40 more, and the pointers from its first row
|
|
100
|
+
function grow(x) {
|
|
101
|
+
W(0xd9 + x, r(0xd9 + x) & 0x7f);
|
|
102
|
+
W(0xd9 + x + 1, r(0xd9 + x + 1) | 0x80);
|
|
103
|
+
W(0xd5, r(0xd5) + 40);
|
|
104
|
+
let k = x; while (!(r(0xd9 + k) & 0x80) && k > 0) k--;
|
|
105
|
+
linePointer(k);
|
|
106
|
+
}
|
|
107
|
+
// ($E87C) on to the next logical line's first row, scrolling at the bottom; ($E891) RETURN, which
|
|
108
|
+
// ends inserts, reverse and quote mode too
|
|
109
|
+
function nextLine() {
|
|
110
|
+
W(0xc9, r(0xc9) >> 1);
|
|
111
|
+
let x = r(0xd6);
|
|
112
|
+
// (the scroll comes back with X its row, $D6, from where the search goes on: $E956)
|
|
113
|
+
do { x++; if (x === 0x19) { scroll(); x = r(0xd6); } } while (!(r(0xd9 + x) & 0x80));
|
|
114
|
+
W(0xd6, x); setLine();
|
|
115
|
+
}
|
|
116
|
+
function carriageReturn() { W(0xd8, 0); W(0xc7, 0); W(0xd4, 0); W(0xd3, 0); nextLine(); }
|
|
117
|
+
// ($E8B3) at the end of a row (column 39 or 79), on to the next row; ($E8A1) at the start of one
|
|
118
|
+
// (column 0 or 40), back to the row before
|
|
119
|
+
function rowEndForward() { for (const end of [0x27, 0x4f]) if (r(0xd3) === end) { if (r(0xd6) !== 0x19) W(0xd6, r(0xd6) + 1); return; } }
|
|
120
|
+
function rowStartBack() { for (const start of [0x00, 0x28]) if (r(0xd3) === start) { W(0xd6, r(0xd6) - 1); return; } }
|
|
121
|
+
// ($EA13) a screen code at the cursor, in colour X, the blink's count 2
|
|
122
|
+
function put(code, colour) { W(0xcd, 2); colourPointer(); w(pnt() + r(0xd3), code); w(user() + r(0xd3), colour); }
|
|
123
|
+
// ($E6B6) the cursor one on: past the line's end, the line grows (to two rows) or a new line
|
|
124
|
+
// (with AUTODN, $0292, set, as while keys wait to be shown, a line opened below: the search for
|
|
125
|
+
// the next line's start from the row the cursor was on, plus one, as $E6CA jumps to $E967 with X
|
|
126
|
+
// as $E8B3 left it, past the LDX $D6 at $E965)
|
|
127
|
+
function advance() {
|
|
128
|
+
const row = r(0xd6);
|
|
129
|
+
rowEndForward();
|
|
130
|
+
W(0xd3, r(0xd3) + 1);
|
|
131
|
+
if (r(0xd5) >= r(0xd3)) return;
|
|
132
|
+
if (r(0xd5) === 0x4f) { W(0xd6, r(0xd6) - 1); nextLine(); W(0xd3, 0); return; }
|
|
133
|
+
if (r(0x292)) return openLine(row + 1);
|
|
134
|
+
let x = r(0xd6);
|
|
135
|
+
if (x >= 0x19) { scroll(); W(0xd6, r(0xd6) - 1); x = r(0xd6); }
|
|
136
|
+
grow(x);
|
|
137
|
+
}
|
|
138
|
+
// a character, quote mode and inserts as the KERNAL keeps them
|
|
139
|
+
function show(code) {
|
|
140
|
+
if (r(0xc7)) code |= 0x80;
|
|
141
|
+
if (r(0xd8)) W(0xd8, r(0xd8) - 1);
|
|
142
|
+
put(code, r(0x286));
|
|
143
|
+
advance();
|
|
144
|
+
}
|
|
145
|
+
function done() { if (r(0xd8)) W(0xd4, r(0xd4) >> 1); }
|
|
146
|
+
// ($E701) back to the end of the logical line before, or nowhere at the top
|
|
147
|
+
function backLine() {
|
|
148
|
+
const x = r(0xd6);
|
|
149
|
+
if (!x) { W(0xd3, 0); return false; }
|
|
150
|
+
W(0xd6, x - 1); setLine(); W(0xd3, r(0xd5)); return true;
|
|
151
|
+
}
|
|
152
|
+
function chrout(ch) {
|
|
153
|
+
W(0xd7, ch); W(0xd0, 0);
|
|
154
|
+
if (ch & 0x80) return shifted(ch);
|
|
155
|
+
if (ch === 0x0d) { carriageReturn(); return done(); }
|
|
156
|
+
if (ch >= 0x20) {
|
|
157
|
+
let code = ch < 0x60 ? ch & 0x3f : ch & 0xdf;
|
|
158
|
+
if (ch === 0x22) W(0xd4, r(0xd4) ^ 1);
|
|
159
|
+
show(code); return done();
|
|
160
|
+
}
|
|
161
|
+
if (r(0xd8)) { show(ch | 0x80); return done(); }
|
|
162
|
+
if (ch === 0x14) { // DEL
|
|
163
|
+
if (r(0xd3) === 0) { if (!backLine()) return done(); }
|
|
164
|
+
else {
|
|
165
|
+
rowStartBack(); W(0xd3, r(0xd3) - 1); colourPointer();
|
|
166
|
+
for (let y = r(0xd3); y + 1 !== r(0xd5) + 1 && y < r(0xd5); y++) { w(pnt() + y, r(pnt() + y + 1)); w(user() + y, r(user() + y + 1)); }
|
|
167
|
+
}
|
|
168
|
+
w(pnt() + r(0xd5), 0x20); w(user() + r(0xd5), r(0x286));
|
|
169
|
+
return done();
|
|
170
|
+
}
|
|
171
|
+
if (r(0xd4)) { show(ch | 0x80); return done(); }
|
|
172
|
+
if (ch === 0x12) W(0xc7, 0x12);
|
|
173
|
+
if (ch === 0x13) home();
|
|
174
|
+
if (ch === 0x1d) { // cursor right
|
|
175
|
+
const y = r(0xd3) + 1; rowEndForward(); W(0xd3, y);
|
|
176
|
+
if (y - 1 >= r(0xd5)) { W(0xd6, r(0xd6) - 1); nextLine(); W(0xd3, 0); }
|
|
177
|
+
return done();
|
|
178
|
+
}
|
|
179
|
+
if (ch === 0x11) { // cursor down
|
|
180
|
+
let y = r(0xd3) + 40; W(0xd6, r(0xd6) + 1);
|
|
181
|
+
if (y > r(0xd5)) { W(0xd6, r(0xd6) - 1); do { y -= 40; if (y >= 0) W(0xd3, y); } while (y > 0 && y >= 40); nextLine(); }
|
|
182
|
+
else W(0xd3, y);
|
|
183
|
+
return done();
|
|
184
|
+
}
|
|
185
|
+
const colour = COLOURS.indexOf(ch); if (colour >= 0) W(0x286, colour);
|
|
186
|
+
if (ch === 0x0e) w(0xd018, r(0xd018) | 2);
|
|
187
|
+
if (ch === 0x08) W(0x291, 0x80); if (ch === 0x09) W(0x291, 0);
|
|
188
|
+
return done();
|
|
189
|
+
}
|
|
190
|
+
function shifted(ch) {
|
|
191
|
+
ch &= 0x7f; if (ch === 0x7f) ch = 0x5e;
|
|
192
|
+
if (ch >= 0x20) { show(ch | 0x40); return done(); }
|
|
193
|
+
if (ch === 0x0d) { carriageReturn(); return done(); }
|
|
194
|
+
if (r(0xd4) || (ch !== 0x14 && r(0xd8))) { show(ch | 0x40 | 0x80); return done(); }
|
|
195
|
+
if (ch === 0x14) { // INST
|
|
196
|
+
let y = r(0xd5);
|
|
197
|
+
if (r(pnt() + y) === 0x20 && y !== r(0xd3)) { /* room at the end */ }
|
|
198
|
+
else if (y === 0x4f) return done();
|
|
199
|
+
else openLine();
|
|
200
|
+
y = r(0xd5); colourPointer();
|
|
201
|
+
for (; y > r(0xd3); y--) { w(pnt() + y, r(pnt() + y - 1)); w(user() + y, r(user() + y - 1)); }
|
|
202
|
+
w(pnt() + y, 0x20); w(user() + y, r(0x286)); W(0xd8, r(0xd8) + 1);
|
|
203
|
+
return done();
|
|
204
|
+
}
|
|
205
|
+
if (ch === 0x11) { // cursor up
|
|
206
|
+
const x = r(0xd6);
|
|
207
|
+
if (x) { W(0xd6, x - 1); const y = r(0xd3) - 40; if (y >= 0) W(0xd3, y); else setLine(); }
|
|
208
|
+
return done();
|
|
209
|
+
}
|
|
210
|
+
if (ch === 0x12) W(0xc7, 0);
|
|
211
|
+
if (ch === 0x1d) { // cursor left
|
|
212
|
+
if (r(0xd3) === 0) backLine(); else { rowStartBack(); W(0xd3, r(0xd3) - 1); }
|
|
213
|
+
return done();
|
|
214
|
+
}
|
|
215
|
+
if (ch === 0x13) { clear(); return done(); }
|
|
216
|
+
const colour = COLOURS.indexOf(ch | 0x80); if (colour >= 0) W(0x286, colour);
|
|
217
|
+
if (ch === 0x0e) w(0xd018, r(0xd018) & ~2);
|
|
218
|
+
return done();
|
|
219
|
+
}
|
|
220
|
+
// The line typed at the keyboard, as CHRIN gives it ($E632): while no line is being read ($D0
|
|
221
|
+
// 0), each key in the buffer shown as CHROUT shows it ($E5CA), the cursor's character put back
|
|
222
|
+
// first if it shows, and SHIFT-RUN/STOP putting LOAD and RUN in the buffer; at RETURN, the
|
|
223
|
+
// logical line the cursor is on read back from the screen ($E602): to its last character that
|
|
224
|
+
// is not a space, from its start, or from where the input began if that was on this line (a
|
|
225
|
+
// prompt's place, $C9/$CA, which CHRIN's caller sets: `begin`). Then a character a call, each
|
|
226
|
+
// screen code made a character again ($E63A), quote mode followed, and RETURN at the end.
|
|
227
|
+
// Gives the character, or null while it waits for a key.
|
|
228
|
+
function begin() { W(0xca, r(0xd3)); W(0xc9, r(0xd6)); }
|
|
229
|
+
function take() { const k = r(0x277); for (let i = 0; i < r(0xc6) - 1; i++) W(0x277 + i, r(0x278 + i)); W(0xc6, r(0xc6) - 1); return k; }
|
|
230
|
+
function input() {
|
|
231
|
+
if (!r(0xd0)) {
|
|
232
|
+
for (;;) {
|
|
233
|
+
const n = r(0xc6); W(0xcc, n); W(0x292, n);
|
|
234
|
+
if (!n) return null;
|
|
235
|
+
if (r(0xcf)) { W(0xcf, 0); W(0xcd, 2); colourPointer(); w(pnt() + r(0xd3), r(0xce)); w(user() + r(0xd3), r(0x287)); }
|
|
236
|
+
const k = take();
|
|
237
|
+
if (k === 0x83) { W(0xc6, 9); [0x4c, 0x4f, 0x41, 0x44, 0x0d, 0x52, 0x55, 0x4e, 0x0d].forEach((v, i) => W(0x277 + i, v)); continue; }
|
|
238
|
+
if (k !== 0x0d) { chrout(k); continue; }
|
|
239
|
+
// RETURN: the line's end, its last character that is not a space, and where to read from
|
|
240
|
+
let y = r(0xd5); W(0xd0, y);
|
|
241
|
+
while (r(pnt() + y) === 0x20 && --y) ;
|
|
242
|
+
W(0xc8, y + 1); W(0x292, 0); W(0xd3, 0); W(0xd4, 0);
|
|
243
|
+
if (!(r(0xc9) & 0x80)) {
|
|
244
|
+
let x = r(0xd6);
|
|
245
|
+
if (x !== r(0xc9)) { while (!(r(0xd9 + x) & 0x80) && x) x--; linePointer(x); }
|
|
246
|
+
if (x === r(0xc9)) { W(0xd3, r(0xca)); if (r(0xca) >= r(0xc8)) return end(); }
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// a character of the line from the screen: its screen code made a character again, the top
|
|
252
|
+
// bit of a reversed one kept in quote mode
|
|
253
|
+
const y = r(0xd3), s = r(pnt() + y);
|
|
254
|
+
let a = s & 0x3f;
|
|
255
|
+
if (s & 0x40) a |= 0x80;
|
|
256
|
+
if (!((s & 0x80) && r(0xd4)) && !(s & 0x20)) a |= 0x40;
|
|
257
|
+
W(0xd3, y + 1);
|
|
258
|
+
if (a === 0x22) W(0xd4, r(0xd4) ^ 1);
|
|
259
|
+
if (y === r(0xc8)) return end();
|
|
260
|
+
W(0xd7, a); // (the character given kept, as $E674 keeps it)
|
|
261
|
+
return a === 0xde ? 0xff : a;
|
|
262
|
+
}
|
|
263
|
+
// the line's end: RETURN, shown only if the input is the screen or the output is not
|
|
264
|
+
function end() { W(0xd0, 0); if (r(0x99) === 3 || r(0x9a) !== 3) chrout(0x0d); W(0xd7, 0x0d); return 0x0d; }
|
|
265
|
+
|
|
266
|
+
return { chrout, clear, home, setLine, clearLine, scroll, linePointer, colourPointer, begin, input };
|
|
267
|
+
}
|