@moonbase-machines/player 0.0.0-stage → 0.9.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/remake/card.js ADDED
@@ -0,0 +1,526 @@
1
+ // The routines of a card title, for the remake engine, as its title.js `map` names them (cards.js):
2
+ // what each step of a script does. The toolkit's own code is lifted into scripts too, the
3
+ // libraries beside the scenes, and a step that is one of its entries runs that code inside
4
+ // itself, so that only the step is in the transcript, as the original's observer sees only the
5
+ // call. What cannot be a script is here: the routines that take their data inline (fill, copy
6
+ // and print), Applesoft's and the Monitor's routines in ROM (@moonbase-machines/apple2e/rom.js), ProDOS's calls
7
+ // (@moonbase-machines/apple2e/mli.js), and the keyboard and the soft switches. The screen is the hi-res pages in the
8
+ // engine's own memory, $2000 and $4000, written byte for byte as the original writes them.
9
+ import { describe, result, TAKES, GIVES } from './events.js';
10
+ import { INLINE, sectionName } from './cards.js';
11
+ import { Files, BLOCK, CLOCK } from '@moonbase-machines/apple2e/mli.js';
12
+ import { install as installRom } from '@moonbase-machines/apple2e/rom.js';
13
+ import { install as installKernal, VIC, VECTORS, kernalByte, KEY_TABLES } from '@moonbase-machines/c64/kernal.js';
14
+ import { Drive } from '@moonbase-machines/c64/drive.js';
15
+ import { Printer, bus } from '@moonbase-machines/c64/printer.js';
16
+ import { Basic } from '@moonbase-machines/c64/basic.js';
17
+ import { BasicError } from '@moonbase-machines/c64/numbers.js';
18
+ import { install as installBasicRom, printError } from '@moonbase-machines/c64/basic-rom.js';
19
+ import { lift, script as scriptOf, readers } from './lift.js';
20
+
21
+ const hex4 = n => n.toString(16).toUpperCase().padStart(4, '0');
22
+
23
+ // The inputs of a step: numbers for the registers it takes, the carry among them, or a
24
+ // register's own name for what it holds already. A number is a load the lifter folded into the
25
+ // step, and the load set the flags, which a routine may test before anything sets them again
26
+ // ($74FA tests A's): they are set as A's load set them, or else as the last register's.
27
+ function inputs(e, name, args) {
28
+ const takes = TAKES[name] || '';
29
+ let k = 0, loaded = null;
30
+ for (const r of takes) {
31
+ const v = args[k++];
32
+ if (v === undefined || v === r || typeof v === 'object') continue;
33
+ const n = v.startsWith('$') ? e.mem[parseInt(v.slice(1), 16)] : +v;
34
+ if (r === 'c') e.c = n & 1; else { e[r] = n & 0xff; if (loaded !== 'a') loaded = r; }
35
+ }
36
+ if (loaded) e.flags(e[loaded]);
37
+ return args.slice(k);
38
+ }
39
+
40
+ export function cardRoutines(title) {
41
+ const toolkit = title.map;
42
+ const c64 = title.disk.kind === 'c64';
43
+ const files = c64 ? null : title.files ??= new Files(title.disk);
44
+ // the soft switches: what the screen shows
45
+ const screen = title.screen ??= { text: 1, mixed: 0, page2: 0, hires: 0 };
46
+ // the keyboard's latch, its high bit set until $C010 is read
47
+ let latch = 0;
48
+
49
+ // The I/O page. The keyboard is read as the machine counts its reads (@moonbase-machines/apple2e/machine.js), every
50
+ // read of $C000 and every touch of $C010 with no key waiting, and two thousand in a run stop it
51
+ // to wait for a key (Engine.polled): so the remake waits where the original's run stopped, and
52
+ // a scene that animates while it polls has run as many frames.
53
+ // The language card, $C080 to $C08F: which of its two 4K banks is at $D000, and whether the card
54
+ // or the ROM is read there. The engine has one memory, so the bank not there is kept aside and
55
+ // swapped in when it is switched to (Pieces of Eight keeps its descriptions in one and its map
56
+ // in the other). The card's write-protect, and the ROM's own bytes, are not kept.
57
+ const lc = { bank: 2, read: 0, aside: new Uint8Array(0x1000) };
58
+ const bankTo = (e, bank) => {
59
+ if (bank === lc.bank) return;
60
+ const here = e.mem.slice(0xd000, 0xe000); e.mem.set(lc.aside, 0xd000); lc.aside = here; lc.bank = bank;
61
+ };
62
+ // The Commodore 64's I/O page, $D000 to $DFFF while the 6510's port at $01 shows it there (and
63
+ // its RAM while not): the VIC's registers, the SID's, the colours, and the two CIAs. The games
64
+ // read the first CIA's timer and clock for their random numbers, which are the seed, as the
65
+ // observer makes them (the pilots' observer); its keyboard and joystick lines read nothing
66
+ // held, since the keys come through GETIN.
67
+ const chip = title.chip = new Uint8Array(0x1000); // (the view paints from it: @moonbase-machines/c64/paint.js)
68
+ const SEEDED = { 4: s => s & 0xff, 5: s => s >> 8 & 0xff, 8: s => (s * 7) & 0xff, 9: s => (s * 13) & 0xff };
69
+ function io64(e, at, v) {
70
+ const r = at & 0xfff;
71
+ if (r < 0x400) { // the VIC, its 64 registers again every 64
72
+ const reg = r & 0x3f;
73
+ // the raster line, which a game waits on (Feeding Time for line 256, Petra's Dream for $D011's
74
+ // top bit) and takes random numbers from with the CIA's: a line on at each read of $D012 or
75
+ // $D011, whose top bit is its ninth, as the observer makes them (the pilots' observer), so
76
+ // that both sides read the same. On the page's clock (`e.clock`), the line the original's
77
+ // time has reached, 63 cycles each, so that a wait for one takes the time it took.
78
+ if (v === undefined && (reg === 0x11 || reg === 0x12)) {
79
+ e.raster = e.clock ? Math.floor(e.cycles / 63) % 312 : ((e.raster ?? 0) + 1) % 312;
80
+ return reg === 0x12 ? e.raster & 0xff : (chip[0x11] & 0x7f) | (e.raster >> 1 & 0x80);
81
+ }
82
+ // which interrupt it is ($D019): the raster's, or none of the VIC's (the CIA's timer)
83
+ if (v === undefined && reg === 0x19) return e.vicFlags ?? 0x70;
84
+ if (v === undefined) return chip[reg];
85
+ chip[reg] = v;
86
+ // a raster interrupt's writes, kept by its line for the view (@moonbase-machines/c64/paint.js), until the
87
+ // raster interrupt is turned off
88
+ // (on the page's clock, the last whole frame's: a frame's writes are kept apart until its
89
+ // raster comes round, routines.before, so that none lingers from a screen before)
90
+ if (e.splitLine !== undefined && reg !== 0x19) { const s = e.clock ? (e.clock.splits ??= new Map()) : (title.splits ??= new Map()); (s.get(e.splitLine) ?? s.set(e.splitLine, new Map()).get(e.splitLine)).set(reg, v); }
91
+ if (reg === 0x1a && !(v & 1)) title.splits?.clear();
92
+ return;
93
+ }
94
+ if (r < 0x800) { const reg = 0x400 | (r & 0x1f); if (v === undefined) return chip[reg]; chip[reg] = v; e.sid?.push([e.cycles, r & 0x1f, v]); return; } // the SID
95
+ if (r < 0xc00) { if (v === undefined) return chip[r] | 0xf0; chip[r] = v & 15; return; } // the colours
96
+ if (r < 0xd00) { // CIA 1
97
+ const reg = r & 15;
98
+ if (v === undefined) return SEEDED[reg] ? SEEDED[reg](title.seed ?? 0x11) : reg < 2 ? 0xff : reg === 0x0d ? 0 : chip[0xc00 | reg];
99
+ chip[0xc00 | reg] = v; return;
100
+ }
101
+ if (v === undefined) return chip[r]; chip[r] = v; // CIA 2, its first port the VIC's bank
102
+ // (a raster interrupt's change of the bank kept with its line too, as $100: a map in a
103
+ // bitmap above, text below, Goldfields' horse ride)
104
+ if ((r & 15) === 0 && e.splitLine !== undefined) { const s = e.clock ? (e.clock.splits ??= new Map()) : (title.splits ??= new Map()); (s.get(e.splitLine) ?? s.set(e.splitLine, new Map()).get(e.splitLine)).set(0x100, v); }
105
+ }
106
+ const ioShown = e => (e.mem[1] & 3) !== 0 && (e.mem[1] & 4) !== 0;
107
+ function io(e, at, v) {
108
+ if (c64 && at >= 0xe000) { if (v === undefined) return kernalByte(at); e.mem[at] = v; return; } // the KERNAL's keyboard tables, RAM under them written
109
+ if (c64) return io64(e, at, v);
110
+ if ((at & 0xfff0) === 0xc080) { const b = at & 15; bankTo(e, b & 8 ? 1 : 2); lc.read = (b & 1) === ((b >> 1) & 1) ? 1 : 0; return 0; }
111
+ if (at >= 0xc050 && at <= 0xc057) { const s = ['text', 'mixed', 'page2', 'hires'][(at - 0xc050) >> 1]; screen[s] = at & 1; return 0; }
112
+ if (at === 0xc000 && v === undefined) {
113
+ // (none, -1, read here and not at a tick, is no key, as on the original, where it is pressed as nothing)
114
+ if (!(latch & 0x80)) { const k = e.key(); if (k !== undefined && k >= 0) latch = k | 0x80; }
115
+ if (++e.idle > 2000) e.polled = true;
116
+ return latch;
117
+ }
118
+ if (at === 0xc010) { const r = latch; if (!(latch & 0x80) && ++e.idle > 2000) e.polled = true; latch &= 0x7f; return r; }
119
+ // the speaker, a click at the cycle it came (Engine.time)
120
+ if (at === 0xc030) { e.clicks.push(e.cycles); return 0; }
121
+ return 0;
122
+ }
123
+
124
+ // Applesoft's and the Monitor's routines, as the machine has them (@moonbase-machines/apple2e/rom.js), on a stand-in
125
+ // for its processor that reads and writes the engine's memory
126
+ let engine = null;
127
+ const cpu = { hooks: new Map(), cycles: 0, a: 0, x: 0, y: 0, z: 0, c: 0, n: 0, v: 0, read: a => engine.mem[a], write: (a, v) => { engine.mem[a] = v & 0xff; }, nz(v) { this.n = v >> 7 & 1; this.z = v ? 0 : 1; } };
128
+ // the bell's clicks, at their cycles from the routine's start, the engine's then (`romFrom`)
129
+ let romFrom = 0;
130
+ if (c64) installKernal({ cpu, read: a => engine.read(a), write: (a, v) => engine.write(a, v), drive: title.bus ??= bus(title.drive ??= new Drive(Uint8Array.from(atob(title.disk.image), ch => ch.charCodeAt(0))), title.printer ??= new Printer({ on: false })), // (the drive and a printer, device 4, on the bus: @moonbase-machines/c64/printer.js)
131
+ // (a key let go before any scan saw it is seen once, as a tap)
132
+ held: () => { const h = engine?.clock?.held; if (h?.up) engine.clock.held = null; if (h) h.seen = true; return h ?? null; },
133
+ call: at => { runNow(engine, at, 'the keyboard read'); Object.assign(cpu, { a: engine.a, x: engine.x, y: engine.y, c: engine.c, z: engine.z, n: engine.n, v: engine.v }); },
134
+ loaded: (name, from, to) => { (title.files64 ??= []).push({ name, from, to }); const script = title.fileScripts?.[name]; if (script) { const l = { script, from, to }; loaded.splice(0, Infinity, ...loaded.filter(k => k.from < l.from || k.to > l.to), l); } } });
135
+ else installRom({ cpu, game: title.disk, sw: { lcRead: 0 }, io: a => io(engine, a), file: n => files.file(n), get files() { return files.files; }, put: (path, f) => files.put(path, f), remove: path => files.remove(path), clicks: { push: c => engine.clicks.push(engine.cycles + c - romFrom) }, log() {}, halted: false });
136
+ const rom = (e, at) => {
137
+ const f = cpu.hooks.get(at);
138
+ // a Commodore 64's BASIC, which MENU runs NOTEBOOK (compiled BASIC) and ZOOTIME (BASIC) with
139
+ if (!f && c64 && at >= 0xa000 && at < 0xc000) throw new Error('a BASIC program (Zoopak\'s NOTEBOOK or ZOOTIME): not written yet in the remake');
140
+ if (!f) throw new Error('no ROM routine at $' + hex4(at));
141
+ engine = e;
142
+ Object.assign(cpu, { a: e.a, x: e.x, y: e.y, c: e.c, z: e.z, n: e.n, v: e.v });
143
+ // a routine that reads the keyboard (RDKEY) waits, with no key, and the step runs again
144
+ const start = romFrom = cpu.cycles;
145
+ const r = f(cpu);
146
+ if (r === 'wait') return 'wait';
147
+ // the time a routine takes, which the original took: the Monitor's WAIT counts out
148
+ // (26 + 27A + 5A²) / 2 cycles, and the bell its 192 clicks
149
+ // and its RTS, as the machine's hook takes it
150
+ e.delay += (cpu.cycles - start + 6) / CLOCK * 1000; e.cycles += cpu.cycles - start + 6;
151
+ Object.assign(e, { a: cpu.a & 0xff, x: cpu.x & 0xff, y: cpu.y & 0xff, c: cpu.c, z: cpu.z, n: cpu.n });
152
+ if (r === 'return') return r; // (a routine that returns from its caller too: $E206 at a statement's end)
153
+ };
154
+
155
+ // Where an address of the lifted code is: a label in a script. The scenes all run at $AD00,
156
+ // and PETPUZZLE is read over SCHOOL's end, so where scripts share an address the script is the
157
+ // file read over it last (`loaded`, each ProDOS read of a script's file, its first and last
158
+ // address, the newest last).
159
+ const loaded = [];
160
+ let index = null;
161
+ // the scripts with a label at an address
162
+ const labelled = (e, at) => {
163
+ if (!index) {
164
+ index = new Map();
165
+ for (const name of Object.keys(e.sources)) {
166
+ const p = e.script(name);
167
+ for (const l of Object.keys(p.labels)) if (/^L[0-9A-F]{4}$/.test(l)) { const a = parseInt(l.slice(1), 16); (index.get(a) ?? index.set(a, []).get(a)).push(name); }
168
+ }
169
+ }
170
+ return index.get(at) ?? [];
171
+ };
172
+ // Code a scene's lift missed, reached through a table or a vector its walk could not follow: on
173
+ // a Commodore 64, lifted at need from the file's bytes in memory where it was loaded, with the
174
+ // same lifter (lift.js), and said, so that the entry goes into the scene's lift.
175
+ const atNeed = (e, at) => {
176
+ // (or code no file put there, which a file's own code made: Goldfields' TITLE unpacks its
177
+ // picture and code to $8000 and jumps to $7F40; from the RAM below BASIC's ROM)
178
+ const f = title.files64?.findLast(f => f.from <= at && at <= f.to) ?? (at >= 0x0200 && at < 0xa000 ? { name: 'MEMORY', from: 0x0200, to: 0x9fff } : null);
179
+ if (!f) return null;
180
+ const bytes = e.mem.slice(f.from, f.to + 1), name = (title.fileScripts?.[f.name] ?? f.name) + '@' + hex4(at);
181
+ const lifted = lift(bytes, f.from, [at], toolkit.routines, new Set(), readers(INLINE));
182
+ e.sources[name] = scriptOf(lifted, bytes, f.from, { selfModified: true });
183
+ index = null;
184
+ loaded.push({ script: name, from: f.from, to: f.to });
185
+ (title.liftedAtNeed ??= []).push(f.name + ' $' + hex4(at));
186
+ console.warn(`lifted at need: ${f.name} from $${hex4(at)} (an entry for its scene's lift)`);
187
+ return name;
188
+ };
189
+ // (on a Commodore 64, a file's scene only while that file is what is loaded there: Goldfields'
190
+ // CODE is at $8018 when the boot calls it, and TEXT/CODE, loaded over it later, has a label there)
191
+ const fileScripts = new Set(Object.values(title.fileScripts ?? {}));
192
+ const present = (names, at) => !c64 ? names : names.filter(n => !fileScripts.has(n) || loaded.some(l => l.script === n && l.from <= at && at <= l.to));
193
+ const find = (e, at) => {
194
+ let names = present(labelled(e, at), at);
195
+ if (!names.length && c64 && atNeed(e, at)) names = present(labelled(e, at), at);
196
+ const over = loaded.findLast(l => names.includes(l.script) && l.from <= at && at <= l.to);
197
+ const name = names.length > 1 ? over?.script : names[0];
198
+ if (!name) throw new Error('no script has $' + hex4(at));
199
+ return [name, 'L' + hex4(at)];
200
+ };
201
+ // an address the ROM's routine answers: above $D000, unless the card is read there and a script
202
+ // has its code there (Pieces of Eight's PO8:BLINK, over HPOSN and DRAW); on a Commodore 64, where
203
+ // the 6510's port shows the KERNAL ($E000 on) or BASIC ($A000 to $BFFF: Tell-A-Tale's numbers,
204
+ // over MENU's code in the RAM under it)
205
+ const romAt = (e, a) => c64 ? (a >= 0xe000 ? (e.mem[1] & 2) !== 0 : a >= 0xa000 && a < 0xc000 ? (e.mem[1] & 3) === 3 : a === 0x73 || a === 0x79) : a >= 0xd000 && !(lc.read && labelled(e, a).length);
206
+ // the stir of START.SYSTEM's wait: its calls are no steps
207
+ const stirring = step => toolkit.stir && step?.at >= toolkit.stir.from && step.at <= toolkit.stir.to;
208
+ const emit = (e, ev, quiet) => { if (quiet) e.inside++; e.emit(ev); if (quiet) e.inside--; };
209
+
210
+ // a return, as the engine's: to the caller, and out of a routine run inside a step
211
+ const back = x => { const f = x.stack.pop(); if (f.name !== x.name) x.enter(f.name); x.pc = f.pc; if (f.inside) { x.inside--; f.done?.(x); } f.after?.(x); };
212
+ // the town's loop, the restart and escape (into LIB4's screens) are the game going on, and run
213
+ // as it, with no return
214
+ const through = new Set(['town', 'restart', 'escape']);
215
+ // The data a step takes inline, read from memory after its call as the original reads it,
216
+ // since a scene may write into it (the name, into the text LIB.VARS prints); the script's own
217
+ // words are what the file held, for a step with no address
218
+ const inline = (e, step, n, args) => {
219
+ if (step?.at == null) return args.map(h => parseInt(h, 16));
220
+ const b = []; for (let i = 0; i < n; i++) b.push(e.mem[(step.at + 3 + i) & 0xffff]); return b;
221
+ };
222
+ const words = b => b.map(v => v.toString(16)).join(' ');
223
+ // an event without where in memory its data is, for a title whose strings are not where the
224
+ // original's are (its map's `unplaced`: Goldfields' SETNAM, its name in BASIC's strings)
225
+ const unplaced = ev => { if (toolkit.unplaced?.includes(ev.step)) { delete ev.x; delete ev.y; } return ev; };
226
+ // a step of the original's ROM: its event, then the routine
227
+ // The KERNAL's calls that go through a vector in RAM, as the KERNAL's jump table does (JMP
228
+ // ($0326) for CHROUT): where the game has pointed one at a routine of its own (Zoopak's MENU
229
+ // draws its text into its bitmap), that routine runs, its lifted code inside the step.
230
+ // (Not LOAD's: the emulator answers LOAD at $F49E, past its vector, as the 1541 does in
231
+ // @moonbase-machines/c64/drive.js, so MENU's fast loader, which talks to a drive's own processor over the
232
+ // serial lines, is set up and never run; the remake does the same.)
233
+ const VECTORED = { chrout: [0x326, 0xf1ca], chrin: [0x324, 0xf157], getin: [0x32a, 0xf13e], save: [0x332, 0xf5ed] };
234
+ const inRom = (name, at) => (e, args, step) => {
235
+ inputs(e, name, args); emit(e, unplaced(describe(name, [], e, undefined, undefined, a => e.mem[a])), stirring(step));
236
+ const v = c64 && VECTORED[name], to = v && (e.mem[v[0]] | e.mem[v[0] + 1] << 8);
237
+ if (v && to !== v[1]) { const [script, label] = find(e, to); e.callInside(script, label, x => { if (GIVES[name]) x.emit(result(name, { a: x.a, x: x.x, y: x.y, c: x.c })); }, step?.at == null ? undefined : (step.at + 2) & 0xffff); return; }
238
+ if (rom(e, at) === 'wait') return 'wait';
239
+ if (GIVES[name] && c64) e.emit(result(name, { a: e.a, x: e.x, y: e.y, c: e.c }));
240
+ };
241
+
242
+ const routines = {
243
+ // fill, copy, print and say, which take their data inline after the call, run their lifted
244
+ // code as the other entries do: the call's frame gives the code its return address to read
245
+ // the data from, and the code returns past it (Engine.pull, Engine.ret)
246
+ // ProDOS: the call and its parameters' address inline; a read of a script's file is noted,
247
+ // where it went, for the scripts that share addresses
248
+ prodos: (e, args, step) => {
249
+ const b = inline(e, step, 3, args), [cmd, lo, hi] = b, params = lo | hi << 8;
250
+ emit(e, describe('prodos', [], e, words(b)), stirring(step));
251
+ const mem = { read: a => e.mem[a], write: (a, v) => { e.mem[a] = v & 0xff; } };
252
+ if (cmd === 0xca) {
253
+ const ref = e.mem[params + 1], o = files.open.get(ref), to = e.mem[params + 2] | e.mem[params + 3] << 8;
254
+ const n = o ? Math.min(e.mem[params + 4] | e.mem[params + 5] << 8, o.f.body.length - o.at) : 0;
255
+ const script = o && title.fileScripts?.[sectionName(toolkit, a => e.mem[a], o.f.path, o.at)];
256
+ if (script && n) {
257
+ const l = { script, from: to, to: to + n - 1 };
258
+ loaded.splice(0, Infinity, ...loaded.filter(k => k.from < l.from || k.to > l.to), l);
259
+ }
260
+ // a disk's time for the blocks read
261
+ e.delay += Math.ceil(n / 512) * BLOCK / CLOCK * 1000;
262
+ }
263
+ let err = files.call(cmd, params, mem);
264
+ e.cycles += 6; // the RTS, as the machine's hook at $BF00 takes it
265
+ if (err === null) err = 1;
266
+ e.a = err === 'quit' ? 0 : err; e.c = e.a ? 1 : 0; e.z = e.a ? 0 : 1;
267
+ if (err === 'quit') e.ended = true;
268
+ },
269
+ // START.SYSTEM's stirring wait is over: the counts it leaves are the seed, as on the original
270
+ stirred: e => { for (const a of toolkit.stir.counts) e.mem[a] = title.seed & 0xff; },
271
+ hposn: inRom('hposn', 0xf411), hlin: inRom('hlin', 0xf53a), hcolor: inRom('hcolor', 0xf6f0), 'clear-page': inRom('clear-page', 0xf3f4),
272
+ pause: inRom('pause', 0xfca8), home: inRom('home', 0xfc58), outport: inRom('outport', 0xfe95),
273
+ // a call or a jump to an address outside the script: into the lifted code where it is, or
274
+ // into the ROM
275
+ 'call-engine': (e, [at], step) => {
276
+ const a = parseInt(at.slice(1), 16);
277
+ if (romAt(e, a)) { const r = rom(e, a); if (r === 'return') { back(e); return; } return r; }
278
+ // (code the game has made a bare RTS since: back at once, the RTS's time taken, as
279
+ // Detect-A-Pet's room hook at $AD03, which it sets to RTS in most rooms)
280
+ if (!c64 && e.mem[a] === 0x60) { e.cycles += 6; return; }
281
+ const [name, label] = find(e, a);
282
+ e.stack.push({ name: e.name, pc: e.pc, ret: step?.at == null ? undefined : (step.at + 2) & 0xffff }); e.enter(name, label);
283
+ },
284
+ // a jump to one of the toolkit's entries is that step: the town's loop and the restart, which
285
+ // never come back
286
+ 'goto-engine': (e, [at], step) => {
287
+ const a = parseInt(at.slice(1), 16), entry = toolkit.routines[a];
288
+ if (entry) {
289
+ if (through.has(entry)) return routines[entry](e, [], step);
290
+ // a jump to a routine that comes back: it returns for the code that jumped, to its caller
291
+ // (the ROM's SYS, on a Commodore 64, which makes it the ROM's call, and no event)
292
+ const quiet = c64 && e.stack.at(-1)?.rom;
293
+ if (quiet) e.inside++;
294
+ let r; try { r = routines[entry](e, [], step); } finally { if (quiet) e.inside--; }
295
+ const f = e.stack.at(-1);
296
+ if (f?.inside) { const done = f.done; f.done = x => { done?.(x); back(x); }; } else back(e);
297
+ return r;
298
+ }
299
+ if (romAt(e, a)) {
300
+ if (rom(e, a) === 'wait') return 'wait';
301
+ if (c64 && ENDS.has(a)) pullRegisters(e); // the end of an interrupt: the registers back, then its RTI
302
+ back(e); return; // as its RTS: out of a routine it ends
303
+ }
304
+ e.enter(...find(e, a));
305
+ },
306
+ };
307
+
308
+ // A Commodore 64 title's BASIC (Goldfields: its boot, GOLDFIELDS, BASIC's text, and its game,
309
+ // GAME, Blitz!'s code), compiled to scripts whose steps the library in @moonbase-machines/c64/basic.js does,
310
+ // the engine's `basic`. SYS runs the machine code at its address, with BASIC's text pointer
311
+ // ($7A) at the text after it and the registers from $030C, as the ROM's SYS does; the machine
312
+ // code reads its parameters from that text with the ROM's routines, which are answered here
313
+ // from the parameters' expressions, compiled with the program (`params`).
314
+ if (c64 && title.basic) {
315
+ routines.sys = (e, at, text, step) => {
316
+ e.mem[0x7a] = text & 0xff; e.mem[0x7b] = text >> 8;
317
+ const m = e.mem; Object.assign(e, { a: m[0x30c], x: m[0x30d], y: m[0x30e], c: m[0x30f] & 1, z: m[0x30f] >> 1 & 1, n: m[0x30f] >> 7 });
318
+ // and as it returns, the registers kept at $030C, as the ROM's SYS keeps them (Goldfields'
319
+ // map reads where the digger may go from them, C = PEEK(780))
320
+ const keep = x => { m[0x30c] = x.a; m[0x30d] = x.x; m[0x30e] = x.y; m[0x30f] = x.c | x.z << 1 | (x.i ?? 0) << 2 | (x.d ?? 0) << 3 | x.v << 6 | x.n << 7; };
321
+ if (romAt(e, at)) { const r = rom(e, at); keep(e); return r; }
322
+ // (the frame is the ROM's SYS, as the machine code's return address is: a jump from the
323
+ // code to the KERNAL is the ROM's call then, as the observer sees it)
324
+ const [name, label] = find(e, at);
325
+ e.stack.push({ name: e.name, pc: e.pc, rom: true, after: keep }); e.enter(name, label);
326
+ };
327
+ // the boot's LOAD: the file, as BASIC loads it from a program, which then runs again from its
328
+ // start with its variables kept; or, for a BASIC program loaded over it (GAME, at $0801),
329
+ // that program from its start
330
+ routines['basic-load'] = (e, [name, device = 1, sa = 0]) => {
331
+ const m = e.mem; for (let k = 0; k < name.length; k++) m[0x200 + k] = name.charCodeAt(k);
332
+ Object.assign(m, { 0xb7: name.length, 0xbb: 0x00, 0xbc: 0x02, 0xba: device, 0xb9: sa, 0xb8: 0 });
333
+ Object.assign(e, { a: 0, x: 0x01, y: 0x08 });
334
+ e.basic.call('load');
335
+ const program = Object.entries(title.basic).find(([, p]) => p.file === name);
336
+ if (sa === 0 && program) { startBasic(e, program[0]); e.enter(program[0]); return; }
337
+ e.pc = 0;
338
+ };
339
+ // BASIC's routines the machine code reads its parameters with, and the rest it calls
340
+ // (@moonbase-machines/c64/basic-rom.js), the parameters' values from the expressions compiled with the program
341
+ installBasicRom({ cpu, read: a => engine.read(a), write: (a, v) => engine.write(a, v),
342
+ evaluate: at => {
343
+ const p = engine.sources[engine.basic.script]?.params?.[at];
344
+ if (!p) throw new Error('no parameter compiled at $' + hex4(at));
345
+ const [end, f, place] = p;
346
+ return { value: f(engine.basic), end, place };
347
+ } });
348
+ }
349
+ // A BASIC error, as the ROM's routine gives it ($A43A, through the vector at $0300 Goldfields
350
+ // leaves the ROM's): the output back to the screen, a new line, ? and the message, ERROR, IN and
351
+ // the "line", which for Blitz!'s code is its program counter at the instruction that failed,
352
+ // then READY.; the program is over. Its output is the ROM's, through CHROUT, and no events.
353
+ if (c64 && title.basic) routines.error = (e, err) => {
354
+ if (!(err instanceof BasicError) || !e.basic) return false;
355
+ engine = e;
356
+ return printError({ cpu, write: (a, v) => { e.mem[a] = v; } }, err.message, e.basic.at);
357
+ };
358
+ const startBasic = (e, name) => { e.basic = new Basic(e, title.basic[name]); e.basic.script = name; };
359
+ // every other entry of the toolkit runs its lifted code inside its step
360
+ for (const [at, name] of Object.entries(toolkit.routines).map(([a, n]) => [+a, n])) {
361
+ // a routine of the ROM's, above $D000, is its step in @moonbase-machines/apple2e/rom.js
362
+ if (!routines[name] && at >= 0xd000) routines[name] = inRom(name, at);
363
+ if (routines[name] || at >= 0xbf00) continue;
364
+ routines[name] = (e, args, step) => {
365
+ inputs(e, name, args);
366
+ // the game going on from inside a routine (Escape read by the key reader): what it was
367
+ // inside is the game's own code now, as the observer's count of calls starts again, and
368
+ // the returns still to come give nothing
369
+ if (through.has(name) && e.inside) { for (const f of e.stack) if (f.inside) { delete f.inside; delete f.done; } e.inside = 0; }
370
+ // its code where the call goes, when another of the same name is elsewhere (LIB4 says its
371
+ // messages by its own $8046, the scenes by LIB2's $144B, a JMP to $1C11): the operand of
372
+ // the call, or else the first of that name
373
+ const to = step?.at != null ? e.mem[(step.at + 1) & 0xffff] | e.mem[(step.at + 2) & 0xffff] << 8 : null;
374
+ const quiet = stirring(step), entry = toolkit.routines[to] === name ? to : parseInt(Object.entries(toolkit.routines).find(([, n]) => n === name)[0]);
375
+ if (name === 'read-key') { e.mem[toolkit.seed] = title.seed & 0xff; e.mem[toolkit.seed + 1] = title.seed >> 8; }
376
+ // the data after the call, as the observer reads it (cards.js)
377
+ const data = INLINE[name] && step?.at != null ? INLINE[name](a => e.mem[a & 0xffff], step.at + 3).value : undefined;
378
+ emit(e, describe(name, [], { a: e.a, x: e.x, y: e.y, c: e.c }, data, undefined, a => e.mem[a]), quiet);
379
+ const [script, label] = find(e, entry);
380
+ // (escape is called, as the original's JSR: LIB4 keeps the stack with its return (TSX at
381
+ // $8E63), and Return to present game goes back through it to where Escape was read)
382
+ if (name === 'escape' && step?.at != null) e.stack.push({ name: e.name, pc: e.pc, ret: (step.at + 2) & 0xffff });
383
+ if (through.has(name)) { e.enter(script, label); e.entered = entry; return; }
384
+ // the key reader's counts of its wait are the seed again as it returns, as on the original
385
+ const seed = x => { x.mem[toolkit.seed] = title.seed & 0xff; x.mem[toolkit.seed + 1] = title.seed >> 8; };
386
+ e.callInside(script, label, x => { if (name === 'read-key') seed(x); if (GIVES[name] && !quiet) x.emit(result(name, { a: x.a, x: x.x, y: x.y, c: x.c })); }, step?.at == null ? undefined : (step.at + 2) & 0xffff);
387
+ };
388
+ }
389
+ // A call or a jump to a label of the script's own that a file read since covers with another
390
+ // script's code: SCHOOL's $B670 on, once PETPUZZLE is read there
391
+ routines.elsewhere = (e, label) => {
392
+ const at = parseInt(label.slice(1), 16), over = loaded.findLast(l => l.from <= at && at <= l.to);
393
+ return over !== undefined && over.script !== e.name && /^L[0-9A-F]{4}$/.test(label);
394
+ };
395
+ // A tick (its map's `ticks`): before the scene's read of the keyboard, the run stops for a key
396
+ // or none (-1), and the read takes it.
397
+ // the original's code is in memory where its lines came from: the engine counts their time
398
+ routines.timed = true;
399
+ // how many polls with no key are a wait (its map's `polls`; a pilot's route can set the
400
+ // engine's own, `e.polls`, for a stretch of keys: the pilots' `pollsAt`)
401
+ routines.polls = toolkit.polls;
402
+ routines.reads = toolkit.reads; // and how many reads of the clock are a tick of it (basic.js `ti`)
403
+ // GETIN on the Commodore 64: the key pressed, or none; called without end while the game waits,
404
+ // so many in a row with no key is the wait, as the observer counts them (the pilots' observer).
405
+ // The KERNAL's buffer first, where the interrupt puts a key it holds down (MENU's), then the
406
+ // page's keys.
407
+ if (c64) routines.getin = e => {
408
+ if (!e.clock && !e.i) { routines.poll(e); e.polledSince = true; }
409
+ let k;
410
+ if (e.mem[0xc6]) { k = e.mem[0x277]; e.mem.copyWithin(0x277, 0x278, 0x277 + e.mem[0xc6]); e.mem[0xc6]--; } else k = e.key();
411
+ e.c = 0;
412
+ if (k === undefined || k < 0) { e.a = e.flags(0); if (++e.idle > (e.polls ?? toolkit.polls ?? 200) && !e.clock) e.polled = true; return; }
413
+ e.a = e.flags(k); e.idle = 0; e.emit(result('getin', { a: k }));
414
+ // (on the page's clock the game runs on as the original did, waiting or not)
415
+ };
416
+ // The Commodore 64's interrupt, through $0314 sixty times a second: the KERNAL's own (the clock),
417
+ // or a game's, which does what the game shows in time (Petra's Dream cycles its flames' colour
418
+ // and plays its sounds; MENU's, which it goes on to, flips its title's two screens and reads the
419
+ // joystick) and goes on to the KERNAL's end of an interrupt, its RTI, where the registers and
420
+ // flags come back. The remake keeps no time of the machine's: it takes one at each call of GETIN,
421
+ // as the observer has the original take it (the pilots' observer), and then the VIC's raster
422
+ // interrupt, where the game has turned it on ($D01A), at the line it set (Find Me's split of its
423
+ // screen into two character sets), $D019 saying which it is. On the page's clock (
424
+ // scenes.js sets `e.clock`), each is taken as the original's chips took it: the CIA's every 16,421
425
+ // cycles of the original's time, the raster's when its line comes (routines.before). Not while
426
+ // the game has them masked (SEI). The routine starts as the KERNAL's own entry ($FF48) leaves
427
+ // it, with A, X and Y pushed, and the KERNAL's end ($EA31, $EA7B, $EA7E, $EA81), or its own PLAs
428
+ // and RTI, pulls them.
429
+ const ENDS = new Set([0xea31, 0xea7b, 0xea7e, 0xea81]);
430
+ // a poll's interrupts, as the observer takes them: the CIA's, then the raster's where it is on
431
+ routines.poll = e => { routines.interrupt(e); if (chip[0x1a] & 1) routines.interrupt(e, true); };
432
+ const pullRegisters = e => {
433
+ for (const r of ['y', 'x', 'a']) { const f = e.stack.pop(); if (f?.a === undefined || f.name !== undefined) throw new Error('the end of an interrupt with its registers not on the stack'); e[r] = f.a; }
434
+ };
435
+ if (c64) routines.interrupt = (e, raster = false) => {
436
+ if (e.i) return;
437
+ const to = e.mem[0x314] | e.mem[0x315] << 8, kept = { a: e.a, x: e.x, y: e.y, n: e.n, z: e.z, c: e.c, v: e.v, d: e.d };
438
+ e.vicFlags = raster ? 0xf1 : 0x70;
439
+ if (raster) e.splitLine = chip[0x12] | (chip[0x11] & 0x80) << 1;
440
+ try {
441
+ if (romAt(e, to)) rom(e, to); // the KERNAL's own routine ($EA31, or past its clock, $EA34)
442
+ else { e.i = 1; runNow(e, to, 'the interrupt routine', x => x.stack.push({ a: kept.a }, { a: kept.x }, { a: kept.y })); }
443
+ } finally { Object.assign(e, kept); e.i = 0; e.vicFlags = e.splitLine = undefined; }
444
+ };
445
+ // the game's code at an address run now to its return, inside whatever is running: the
446
+ // interrupt's routine, or one the KERNAL calls through a vector
447
+ function runNow(e, at, what, then) {
448
+ let over = false;
449
+ e.callInside(...find(e, at), () => { over = true; });
450
+ then?.(e);
451
+ for (let n = 0; !over; n++) {
452
+ if (n > 1e6) throw new Error(what + ' at $' + hex4(at) + ' runs on without end');
453
+ if (e.exec(e.program.steps[e.pc]) === 'wait') throw new Error(what + ' at $' + hex4(at) + ' waits');
454
+ }
455
+ }
456
+ // the length of the data after a step's call, the lifted lines leave out (Engine.cost)
457
+ routines.inline = (e, step) => INLINE[step.words[0]] ? INLINE[step.words[0]](a => e.mem[a & 0xffff], step.at + 3).length : 0;
458
+ // And a routine of the title's, reached by a jump or a branch with nothing running (Pieces of
459
+ // Eight jumps to its description routine, $0A59): its step, as the observer's hook makes it
460
+ // wherever the routine is reached from, and the frame on top, the caller of the code that
461
+ // jumped, which the routine returns through, ends it.
462
+ routines.before = (e, step) => {
463
+ if (c64 && e.clock && !e.i) {
464
+ if (e.cycles >= e.clock.irq) { e.clock.irq = Math.max(e.clock.irq + 16421, e.cycles); routines.interrupt(e); }
465
+ // the raster's, when the line it waits for has come since the last step
466
+ const line = Math.floor(e.cycles / 63) % 312, last = e.clock.line ?? line, target = chip[0x12] | (chip[0x11] & 0x80) << 1;
467
+ e.clock.line = line;
468
+ if (line < last) { title.splits = e.clock.splits ?? new Map(); e.clock.splits = new Map(); } // a frame round: its splits the view's
469
+ if (chip[0x1a] & 1 && (last <= line ? last < target && target <= line : target > last || target <= line)) routines.interrupt(e, true);
470
+ }
471
+ // (the town's, the restart's and escape's own steps go on into their code: not twice)
472
+ const at = step.start ?? step.at, named = at != null && e.inside === 0 ? toolkit.routines[at] : undefined, own = e.entered === at;
473
+ e.entered = null;
474
+ if (named && !own && !stirring(step)) {
475
+ emit(e, describe(named, [], { a: e.a, x: e.x, y: e.y, c: e.c }, undefined, undefined, a => e.mem[a]));
476
+ const f = e.stack.at(-1);
477
+ if (f?.name !== undefined && !f.inside) { f.inside = true; f.done = x => { if (GIVES[named]) x.emit(result(named, { a: x.a, x: x.x, y: x.y, c: x.c })); }; e.inside++; }
478
+ }
479
+ const t = toolkit.ticks?.find(t => t.at === step.at && e.name === title.fileScripts?.[t.scene]);
480
+ if (!t) return;
481
+ if (e.ticking === step) { e.ticking = null; if (e.keys[0] === -1) e.keys.shift(); return; }
482
+ e.emit({ step: 'tick' }); e.ticking = step;
483
+ return 'wait';
484
+ };
485
+ // before the start: the files the machine puts in memory itself (START.SYSTEM, where ProDOS
486
+ // would have read it), the bytes it gives (MACHID), and the I/O page
487
+ routines.setup = e => {
488
+ // a Commodore 64 as it is when the game's boot program starts: the 6510's port showing the
489
+ // ROMs and the I/O page, the VIC as the KERNAL set it up and its bank the first, the screen at
490
+ // $0400 in light blue, device 8 and secondary address 1 the last used; and the boot loaded where it goes, over the stack
491
+ if (c64) {
492
+ // (the compiled scenes reach only the title's `io` through the hook: it must hold all this)
493
+ if (!title.io || title.io[0] > 0xd000 || title.io[1] < KEY_TABLES[1] - 1) throw new Error('a Commodore 64 title\'s io must cover $D000 to the KERNAL\'s keyboard tables');
494
+ e.ioAt = at => (at >= 0xd000 && at <= 0xdfff && ioShown(e)) || (at >= KEY_TABLES[0] && at < KEY_TABLES[1] && (e.mem[1] & 2) !== 0);
495
+ Object.assign(e.mem, { 0: 0x2f, 1: 0x37, 0x99: 0, 0x9a: 3, 0xb9: 1, 0xba: 8, 0x288: 4, 0x286: 14 }); // (input from the keyboard, output to the screen; LOAD"ZOOPAK",8,1 left device 8, secondary address 1)
496
+ // the keyboard as the KERNAL keeps it: no key down, the cursor not blinking, a buffer of ten,
497
+ // the repeat's counts, and the decode's vector its own
498
+ Object.assign(e.mem, { 0xc5: 0x40, 0xcb: 0x40, 0xcc: 1, 0x289: 10, 0x28a: 0, 0x28b: 4, 0x28c: 0x10, 0x28f: 0x48, 0x290: 0xeb });
499
+ VIC.forEach((v, i) => { chip[i] = v; }); chip[0xd00] = 0x97;
500
+ VECTORS.forEach((v, i) => { e.mem[0x314 + 2 * i] = v & 0xff; e.mem[0x315 + 2 * i] = v >> 8; }); // the KERNAL's own vectors
501
+ for (let i = 0; i < 1000; i++) { e.mem[0x400 + i] = 0x20; chip[0x800 + i] = 14; }
502
+ for (const [name, at] of Object.entries(title.load ?? {})) { const raw = title.drive.find(Uint8Array.from(name, ch => ch.charCodeAt(0))).raw; e.mem.set(raw.subarray(2), at); }
503
+ e.io = (at, v) => io(e, at, v);
504
+ // a program in BASIC starts from BASIC's READY, RUN typed: the cursor's blink as it was then
505
+ if (title.basic) { Object.assign(e.mem, { 0xcd: 2, 0xce: 0x20, 0xcf: 0, 0x287: 0xfe }); startBasic(e, title.start); }
506
+ return;
507
+ }
508
+ for (const [path, at] of Object.entries(title.load ?? {})) e.mem.set(files.file('/' + files.volumes[0] + '/' + path).body, at);
509
+ // a DOS 3.3 disk: what its Applesoft program did, as the machine does it (@moonbase-machines/apple2e/machine.js boot):
510
+ // each file loaded where the program puts it, the language card's bank as it says, then the
511
+ // card read and written in bank 1, the screen cleared, the text window from row 16, and the
512
+ // CALL; in the time the disk took
513
+ if (title.disk.kind === 'dos33') {
514
+ for (let r = 0; r < 24; r++) e.mem.fill(0xa0, 0x400 + (r & 7) * 0x80 + (r >> 3) * 40, 0x400 + (r & 7) * 0x80 + (r >> 3) * 40 + 40);
515
+ for (const [name, at, bank] of title.disk.loads) {
516
+ if (bank) bankTo(e, bank);
517
+ e.mem.set(files.file(name).body, at);
518
+ }
519
+ bankTo(e, 1); lc.read = 1;
520
+ Object.assign(e.mem, { 0x20: 0, 0x21: 40, 0x22: 16, 0x23: 24, 0x32: 0xff });
521
+ e.delay = 2500;
522
+ }
523
+ e.io = (at, v) => io(e, at, v);
524
+ };
525
+ return routines;
526
+ }
@@ -0,0 +1,47 @@
1
+ // What the card engine (card.js) knows of every title it plays, the title's own map of its routines aside (its
2
+ // title.js `map`, below): the data some routines take inline after the call, a toolkit's sections file, and the
3
+ // Commodore 64's KERNAL as a C64 title's map names it.
4
+ //
5
+ // A title's `map` (its title.js): the routines its scenes call, by address, each with the name of the step it is in
6
+ // a scene's script (`routines`); and what its engine needs beside them, as the title has it: Detect-A-Pet's `seed`
7
+ // (where the key reader takes the random routine's seed from), `stir` (a wait for a key that stirs the random
8
+ // routine as it polls, its counts fixed when the key comes), `ticks` (a scene's own loop that reads the keyboard each
9
+ // frame, each pass a step `tick`), `sections` (its toolkit's sections file, below); a C64 title's `c64`, `start`,
10
+ // `quiet` (routines polled without end, an event only when they give something), `basic`, `polls` and `unplaced`.
11
+
12
+ // The data some entries take inline after the call ({ length, value }), read through `read`: fill's seven bytes,
13
+ // copy's eight and a ProDOS call's three, as hexadecimal; print's ASCII, ending $00; a message on the text screen,
14
+ // `say`, its text ending $00, each character's high bit turned over
15
+ const bytes = n => (read, at) => { const b = []; for (let k = 0; k < n; k++) b.push(read(at + k).toString(16)); return { length: n, value: b.join(' ') }; };
16
+ export const INLINE = {
17
+ fill: bytes(7), copy: bytes(8), prodos: bytes(3),
18
+ 'boot-say': (read, at) => INLINE.say(read, at),
19
+ say(read, at) { let s = '', i = at; for (let guard = 0; guard < 1024; guard++) { const c = read(i++); if (!c) break; s += String.fromCharCode((c ^ 0x80) & 0x7f); } return { length: i - at, value: s }; },
20
+ print(read, at) { let s = '', i = at; for (let guard = 0; guard < 256; guard++) { const c = read(i++); if (!c) break; s += String.fromCharCode(c & 0x7f); } return { length: i - at, value: s }; },
21
+ };
22
+
23
+ // A section of a toolkit's sections file (its map's `sections`: { file, lib, origin, lengths, marks, count }), by
24
+ // X: where it starts in the file and how long it is, from the library's tables, through `read` (memory, with the
25
+ // library there)
26
+ export function section(toolkit, read, x) {
27
+ const { lengths, marks } = toolkit.sections, word = at => read(at + x) | read(at + x + 1) << 8;
28
+ return { mark: word(marks), length: word(lengths) };
29
+ }
30
+ // the name of what a read of `path` from `mark` is: a section's `path:x`, or the path
31
+ export function sectionName(toolkit, read, path, mark) {
32
+ const s = toolkit.sections;
33
+ if (!s || path !== s.file) return path;
34
+ for (let x = 0; x < s.count; x += 2) { const t = section(toolkit, read, x); if (t.length && t.mark === mark) return path + ':' + x; }
35
+ return path;
36
+ }
37
+
38
+ // The KERNAL's jump table at $FFxx as a C64 title calls it, each entry a step answered in JavaScript
39
+ // (@moonbase-machines/c64/kernal.js): the routines Zoopak's code calls. (Not UDTIM, $FFEA: only an interrupt routine
40
+ // calls it, which the remake, taking keys and having no interrupts, does not run.) A title that calls more names them
41
+ // beside these in its own map, as Goldfields does its files'.
42
+ export const KERNAL = {
43
+ 0xffd2: 'chrout', 0xffe4: 'getin', 0xfff0: 'plot', 0xffcc: 'clrchn', 0xffe1: 'stop', 0xffb7: 'readst',
44
+ 0xffba: 'setlfs', 0xffbd: 'setnam', 0xffd5: 'load', 0xffd8: 'save',
45
+ 0xffb1: 'listen', 0xff93: 'second', 0xffa8: 'ciout', 0xffae: 'unlsn', 0xffb4: 'talk', 0xff96: 'tksa', 0xffa5: 'acptr', 0xffab: 'untlk',
46
+ 0xffdb: 'settim', 0xffde: 'rdtim', 0xff90: 'setmsg',
47
+ };