@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/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
+ }