@moonbase-machines/player 0.0.0-stage → 0.8.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 +40 -2
- package/audio.js +54 -0
- package/bin/moonbase-compile.mjs +23 -0
- package/bin/moonbase-play.mjs +75 -0
- package/data.js +39 -0
- package/disk/dialogs.js +101 -0
- package/disk/disk.js +57 -0
- package/disk/local.js +28 -0
- package/disk/memory.js +13 -0
- package/dist/moonbase-player.js +33 -0
- package/dist/moonbase-player.js.map +7 -0
- package/index.js +28 -0
- package/modal.js +68 -0
- package/original.js +200 -0
- package/package.json +25 -4
- package/parts.js +33 -0
- package/player.css +48 -0
- package/remake/c64-view.js +30 -0
- package/remake/card-view.js +53 -0
- package/remake/card.js +526 -0
- package/remake/cards.js +47 -0
- package/remake/compile.js +179 -0
- package/remake/engine.js +461 -0
- package/remake/events.js +79 -0
- package/remake/layout.js +144 -0
- package/remake/lift.js +197 -0
- package/remake/load.js +55 -0
- package/remake/main.js +51 -0
- package/remake/problems.js +43 -0
- package/remake/raster.js +72 -0
- package/remake/scenes.js +243 -0
- package/remake/script.js +60 -0
- package/remake/vector.js +363 -0
- package/remake/view.js +479 -0
- package/test/disk.test.js +65 -0
- package/test/load.test.js +56 -0
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
|
+
}
|
package/remake/cards.js
ADDED
|
@@ -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
|
+
};
|