@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.
@@ -0,0 +1,179 @@
1
+ // A scene script compiled to JavaScript: moonbase-compile writes each title's scripts as modules
2
+ // beside them (its folder's compiled/<name>.js), and the remake runs those.
3
+ //
4
+ // The script's statements become JavaScript on the engine's registers, flags, stack and memory,
5
+ // each line with the script's line beside it, so the code can be read against the original's.
6
+ // A run of statements that does nothing but compute is one block, one function, from a label
7
+ // or the step after a jump, a call or a routine's step, to the next of those; a branch back to
8
+ // the block's own start is a loop in it. What can stop the run stays a step of its own, as the
9
+ // engine runs it: a routine's step (a wait for a key, a picture), and any read or write that may
10
+ // reach the I/O page (the keyboard, read until a key comes, or a tick), so that the title waits
11
+ // where it waited, step for step.
12
+ //
13
+ // Each run of lines to a branch starts with its time (Engine.time), the cycles of the original's
14
+ // instructions there, and a branch taken adds one, as the 6502's does.
15
+ //
16
+ // A compiled script is { labels, data, steps, run }: the labels by step, the data put into
17
+ // memory as the script is read, each step as [its address, its line in the scene, its text],
18
+ // and `run`, each block's function by the index of the step it starts at: (e, s) => ..., on the
19
+ // engine e and the script's steps s.
20
+ import { parse, operand, branchTaken } from './script.js';
21
+
22
+ const hex = (v, n = 2) => '0x' + v.toString(16).padStart(n, '0');
23
+ // the I/O page, the Apple's $C000 to $C0FF unless the title gives its own (compile's `io`)
24
+ let IO_FROM = 0xc000, IO_TO = 0xc0ff;
25
+ const IO = at => at >= IO_FROM && at <= IO_TO;
26
+
27
+ // an operand's address as JavaScript, and whether it may be on the I/O page: an address the
28
+ // code keeps (($26)[y], [$0D19][x]) can be anywhere, and so can a table indexed across $C000
29
+ function address(o) {
30
+ if (o.through !== undefined) return { js: `(e.mem[${hex(o.through)}] | e.mem[${hex(o.through + 1)}] << 8) + e.y & 0xffff`, io: true };
31
+ if (o.pair !== undefined) return { js: `e.mem[${hex(o.pair)} + e.x & 0xff] | e.mem[${hex(o.pair)} + e.x + 1 & 0xff] << 8`, io: true };
32
+ if (o.via !== undefined) return { js: `(e.mem[${hex(o.via, 4)}] | e.mem[${hex(o.via + 1, 4)}] << 8)${o.index ? ' + e.' + o.index + ' & 0xffff' : ''}`, io: true };
33
+ if (!o.index) return { js: hex(o.at, 4), io: IO(o.at) };
34
+ const io = o.at <= IO_TO && o.at + 255 >= IO_FROM;
35
+ return { js: o.at + 255 > 0xffff ? `(${hex(o.at, 4)} + e.${o.index}) & 0xffff` : `${hex(o.at, 4)} + e.${o.index}`, io };
36
+ }
37
+ // reading an operand, and writing one: { js, io }
38
+ // a read for a load, a compare or the like takes a cycle more where an index carries into the
39
+ // next page (Engine.ix); a read for a change in place (an increment, a shift) never does
40
+ function get(word, timed = true) {
41
+ const o = operand(word);
42
+ if (o.number !== undefined) return { js: String(o.number & 0xff) };
43
+ if (o.reg === 'carry') return { js: 'e.c' };
44
+ if (o.reg) return { js: 'e.' + o.reg };
45
+ const a = address(o);
46
+ if (timed && o.through !== undefined) return { js: `e.read(e.ix(e.mem[${hex(o.through)}] | e.mem[${hex(o.through + 1)}] << 8, e.y))`, io: true };
47
+ if (timed && o.via !== undefined && o.index) return { js: `e.read(e.ix(e.mem[${hex(o.via, 4)}] | e.mem[${hex(o.via + 1, 4)}] << 8, e.${o.index}))`, io: true };
48
+ if (timed && o.index && !o.zp) return { js: a.io ? `e.read(e.ix(${hex(o.at, 4)}, e.${o.index}))` : `e.mem[e.ix(${hex(o.at, 4)}, e.${o.index})]`, io: a.io };
49
+ return { js: a.io ? `e.read(${a.js})` : `e.mem[${a.js}]`, io: a.io };
50
+ }
51
+ function put(word, v) {
52
+ const o = operand(word);
53
+ if (o.reg) return { js: `e.${o.reg} = ${v}` };
54
+ const a = address(o);
55
+ return { js: a.io ? `e.write(${a.js}, ${v})` : `e.mem[${a.js}] = ${v}`, io: a.io };
56
+ }
57
+ // a read, then a write of the same place: { at, js(read), write(v), io }
58
+ function place(word) {
59
+ const o = operand(word);
60
+ if (o.reg) return { pre: '', read: 'e.' + o.reg, write: v => `e.${o.reg} = ${v}` };
61
+ const a = address(o);
62
+ if (a.io) return { pre: `const at = ${a.js}; `, read: 'e.read(at)', write: v => `e.write(at, ${v})`, io: true };
63
+ return { pre: '', read: `e.mem[${a.js}]`, write: v => `e.mem[${a.js}] = ${v}` };
64
+ }
65
+
66
+ const FLAGS = { zero: 'e.z', carry: 'e.c', negative: 'e.n', overflow: 'e.v' };
67
+ const TESTS = { '==': 'e.z', '!=': '!e.z', '>=': 'e.c', '<': '!e.c' };
68
+
69
+ // One step as JavaScript: { js, kind }. A statement's kind is 'compute', or 'io' if it may reach
70
+ // the I/O page; a jump, a call or a return ends a block ('end'); a branch may leave it
71
+ // ('branch', with its label); a routine's step is a block of its own ('routine').
72
+ function statement(step, k) {
73
+ const w = step.words, [first, second] = w, s = `s[${k}]`;
74
+ const of = (...parts) => ({ js: parts.map(p => p.js ?? p).join(''), kind: parts.some(p => p.io) ? 'io' : 'compute' });
75
+ if (second === '=' && w.length === 3) {
76
+ const v = get(w[2]), o = operand(first);
77
+ if (o.reg === 'carry') return of('e.c = ', v, ' & 1;');
78
+ if (o.reg) return of(`e.${o.reg} = e.flags(`, v, ');'); // a load sets the flags, a store does not
79
+ const p = put(first, v.js);
80
+ return { js: p.js + ';', kind: v.io || p.io ? 'io' : 'compute' };
81
+ }
82
+ if (second === '+=' || second === '-=') {
83
+ const v = get(w[2]);
84
+ if (w.length === 3) { // an increment or a decrement
85
+ const p = place(first);
86
+ return { js: `${p.pre}${p.write(`e.flags(${p.read} ${second[0]} ${v.js} & 0xff)`)};`, kind: p.io || v.io ? 'io' : 'compute' };
87
+ }
88
+ // with the carry, as ADC and SBC: SBC adds the operand turned over
89
+ return { js: second === '+=' ? `e.add(${v.js});` : `e.sub(${v.js});`, kind: v.io ? 'io' : 'compute' };
90
+ }
91
+ if (second === '&=' || second === '|=' || second === '^=') { const v = get(w[2]); return of(`e.a = e.flags(e.a ${second[0]} `, v, ');'); }
92
+ if (second === '<<=' || second === '>>=' || second === 'rotate') {
93
+ const p = place(first), left = second === '<<=' || w[2] === 'left';
94
+ return { js: `${p.pre}${p.write(`e.shift(${p.read}, ${left}, ${second === 'rotate'})`)};`, kind: p.io ? 'io' : 'compute' };
95
+ }
96
+ switch (first) {
97
+ case 'if': {
98
+ const to = w.at(-1), cond = w.slice(1, -2);
99
+ if (cond.length === 3 && /^[axy]$/.test(cond[0])) { const v = get(cond[2]); return { js: `e.compare(e.${cond[0]}, ${v.js}); if (${TESTS[cond[1]]})`, kind: 'branch', to, io: v.io }; }
100
+ const not = cond[0] === 'not', flag = FLAGS[not ? cond[1] : cond[0]];
101
+ if (!flag) throw new Error(step.where + ': no flag ' + cond.join(' '));
102
+ return { js: `if (${not ? '!' : ''}${flag})`, kind: 'branch', to };
103
+ }
104
+ case 'goto':
105
+ if (w[1].startsWith('(')) return { js: `return e.jumpThrough(${hex(parseInt(w[1].slice(2, -1), 16), 4)}, ${s});`, kind: 'end' };
106
+ if (w[1].startsWith('[')) return { js: `return e.jumpTable(${hex(parseInt(w[1].slice(2), 16), 4)}, e.${w[2][0]});`, kind: 'end' };
107
+ return { js: `return e.jump('${w[1]}', ${s});`, kind: 'end' };
108
+ case 'call':
109
+ if (w[1].startsWith('(')) return { js: `return e.callThrough(${hex(parseInt(w[1].slice(2, -1), 16), 4)}, ${s});`, kind: 'end' };
110
+ return { js: `return e.call('${w[1]}', ${s});`, kind: 'end' };
111
+ case 'return': return { js: `return e.ret(${s});`, kind: 'end' };
112
+ case 'compare': return of(`e.compare(e.${w[1]}, `, get(w[2]), ');');
113
+ case 'test': return of('e.test(', get(w[1]), ');');
114
+ case 'push': return { js: `e.push(${w[1] === 'a'});`, kind: 'compute' };
115
+ case 'pull': return { js: `e.pull(${w[1] === 'a'});`, kind: 'compute' };
116
+ case 'asm': if (w[1] === 'RTI') return { js: `return e.ret(${s});`, kind: 'end' };
117
+ return { js: ['TSX', 'TXS', 'CLV', 'SED', 'CLD', 'SEI', 'CLI'].includes(w[1]) ? `e.asm('${w[1]}');` : '', kind: 'compute' };
118
+ }
119
+ const args = w.slice(1).map(a => typeof a === 'string' ? `'${a}'` : `{ string: ${JSON.stringify(a.string)} }`).join(', ');
120
+ return { js: `return e.routine('${first}', [${args}], ${s});`, kind: 'routine' };
121
+ }
122
+
123
+ export function compile(source, name, { io = [0xc000, 0xc0ff] } = {}) {
124
+ [IO_FROM, IO_TO] = io;
125
+ const { steps, labels, data } = parse(source, name);
126
+ const code = steps.map((step, k) => statement(step, k));
127
+ const starts = new Set([0, ...Object.values(labels)]);
128
+ code.forEach((c, k) => {
129
+ if (c.kind === 'routine' || c.kind === 'io' || c.io) starts.add(k);
130
+ if (c.kind !== 'compute' && c.kind !== 'branch' || c.io) starts.add(k + 1);
131
+ });
132
+ const at = Object.fromEntries(Object.entries(labels).map(([l, k]) => [k, l]));
133
+ const blocks = [];
134
+ for (let k = 0; k < steps.length; k++) {
135
+ if (!starts.has(k)) continue;
136
+ let end = k + 1;
137
+ while (end < steps.length && !starts.has(end)) end++;
138
+ // a branch back to the block's own start: the block is a loop
139
+ const loops = code.slice(k, end).some(c => c.kind === 'branch' && labels[c.to] === k);
140
+ const lines = [];
141
+ for (let i = k; i < end; i++) {
142
+ // the time of the lines to the next branch, or the block's end (a routine's step counts its
143
+ // own call)
144
+ if ((i === k || code[i - 1].kind === 'branch') && code[i].kind !== 'routine') {
145
+ let j = i; while (j < end - 1 && code[j].kind !== 'branch') j++;
146
+ lines.push(`e.time(s, ${i}, ${j});`);
147
+ }
148
+ const c = code[i], text = steps[i].at === null ? steps[i].text : steps[i].at.toString(16).toUpperCase().padStart(4, '0') + ' ' + steps[i].text;
149
+ let js = c.js;
150
+ if (c.kind === 'branch') {
151
+ const t = labels[c.to];
152
+ const more = branchTaken(steps[i]), cy = more === 1 ? 'e.cycles++' : `e.cycles += ${more}`;
153
+ js += t === undefined ? ` { ${cy}; e.pc = e.label('${c.to}'); return; }` : t === k && loops ? ` { ${cy}; continue; }` : ` { ${cy}; e.pc = ${t}; return; }`;
154
+ }
155
+ if (c.kind === 'end') js = `e.pc = ${i + 1}; ` + js;
156
+ lines.push(js ? `${js.padEnd(48)} // ${text}` : `// ${text}`);
157
+ }
158
+ if (code[end - 1].kind !== 'end' && code[end - 1].kind !== 'routine' && end !== k + 1) lines.push(`e.pc = ${end};`);
159
+ const label = at[k] ? ` // ${at[k]}\n` : '';
160
+ // a step alone that gives what it returns: one line
161
+ const one = lines.length === 1 && /^return (.*?);( +\/\/ .*)$/.exec(lines[0]);
162
+ if (one) { blocks.push(`${label} ${`${k}: (e, s) => ${one[1]},`.padEnd(60)}${one[2].trim() ? ' ' + one[2].trim() : ''}`); continue; }
163
+ const body = loops ? ['for (;;) {', ...lines.map(l => ' ' + l), ' break;', '}'] : lines;
164
+ blocks.push(`${label} ${k}: (e, s) => {\n${body.map(l => ' ' + l).join('\n')}\n },`);
165
+ }
166
+ return [
167
+ `// ${name}, compiled from ${name}.scene by moonbase-compile (@moonbase-machines/player/remake/compile.js): compiled`,
168
+ `// again from the scene, never edited`,
169
+ `export default {`,
170
+ ` labels: ${JSON.stringify(labels)},`,
171
+ ` data: [${data.map(d => `\n [${hex(d.at, 4)}, [${d.bytes.map(b => hex(b)).join(', ')}]],`).join('')}\n ],`,
172
+ ` steps: [${steps.map(s => `\n [${s.at === null ? 'null' : hex(s.at, 4)}, ${+s.where.split(':').at(-1)}, ${JSON.stringify(s.text)}],`).join('')}\n ],`,
173
+ ` run: {`,
174
+ ...blocks,
175
+ ` },`,
176
+ `};`,
177
+ '',
178
+ ].join('\n');
179
+ }
@@ -0,0 +1,461 @@
1
+ // The remake engine: it runs a title's scene scripts, and everything a scene does is an event.
2
+ // It draws nothing itself. A view listens to the events and draws them however it likes, and a
3
+ // pilot reads them as the transcript to hold against the original's.
4
+ //
5
+ // A script's statements act on three registers, a, x and y, four flags, a stack, and 64K of
6
+ // memory, as the lines they were lifted from did, so the logic of a scene is kept exactly. Its
7
+ // steps are the engine's work, which the title's routines do: a text, a picture, a wait.
8
+ //
9
+ // const e = new Engine({ routines, scripts, start })
10
+ // e.run() runs until the title waits for a key: 'idle', or 'end'
11
+ // e.press(key) a key, as a character code; the waiting step takes it on the next run
12
+ // e.on(event => ...)
13
+ // e.onKey(key => ...) each key as the title's keyboard takes it (key()), apart from the events, which stay the
14
+ // transcript; returns a function that stops it
15
+ import { parse, operand, words, branchTaken } from './script.js';
16
+ import { CYCLES } from '@moonbase-machines/mos6502/cpu.js';
17
+
18
+ // what a routine throws when the title leaves what it was doing (Engine.leave)
19
+ const LEFT = { left: true };
20
+ // the words that start a statement rather than a routine's step
21
+ const STATEMENTS = new Set(['if', 'goto', 'call', 'return', 'compare', 'test', 'asm', 'push', 'pull']);
22
+ const STATEMENT = step => STATEMENTS.has(step.words[0]) || /^(=|\+=|-=|&=|\|=|\^=|<<=|>>=|rotate)$/.test(step.words[1]);
23
+ const CYCLES_PER_MS = 1020.484;
24
+ // an instruction's size by its opcode, the 6502's: by the addressing mode its bits give
25
+ function size(op) {
26
+ if (op === 0x20) return 3;
27
+ if (op === 0x00 || op === 0x40 || op === 0x60) return 1;
28
+ const b = op >> 2 & 7, cc = op & 3;
29
+ if (cc === 1) return b === 3 || b === 6 || b === 7 ? 3 : 2;
30
+ if (b === 0) return cc === 2 || (op & 0xe0) >= 0xa0 ? 2 : 1;
31
+ if (b === 2 || b === 6) return cc === 0 || cc === 2 ? 1 : 2;
32
+ if (b === 4) return cc === 0 ? 2 : 2;
33
+ return b === 3 || b === 7 ? 3 : 2;
34
+ }
35
+ // what the lifter leaves out of its lines: a NOP, and a load of A, X or Y, immediate, zero page
36
+ // or absolute, or CLC or SEC, that it takes into the step after as its input
37
+ const LEFT_OUT = new Set([0xea, 0xa9, 0xa2, 0xa0, 0xa5, 0xa6, 0xa4, 0xad, 0xae, 0xac, 0x18, 0x38]);
38
+ const COMPARED = step => step.words[0] === 'if' && step.words.length === 6;
39
+ // the return address a call leaves on the 6502's stack, its JSR's last byte
40
+ const returnOf = step => step?.at == null ? undefined : (step.at + 2) & 0xffff;
41
+ // an address as a script's label, L8157
42
+ export const label16 = at => 'L' + at.toString(16).toUpperCase().padStart(4, '0');
43
+
44
+ export class Engine {
45
+ constructor({ routines, scripts, start, label, memory = {} }) {
46
+ this.routines = routines; this.sources = scripts;
47
+ this.mem = new Uint8Array(0x10000);
48
+ for (const [at, v] of Object.entries(memory)) this.mem[at] = v;
49
+ this.a = 0; this.x = 0; this.y = 0;
50
+ this.n = 0; this.z = 0; this.c = 0; this.v = 0; this.d = 0;
51
+ this.stack = []; this.keys = []; this.listeners = []; this.takers = []; this.ended = false;
52
+ // the time the original took, in its cycles, for a title whose code is in memory where its
53
+ // lines came from (`timed`): each line its instruction's, and the speaker's clicks by it
54
+ this.timed = !!routines.timed; this.cycles = 0; this.clicks = []; this.delay = 0;
55
+ this.programs = {};
56
+ // what the title's routines set up before the start: memory, the I/O page (`io`)
57
+ routines.setup?.(this);
58
+ this.enter(start, label, true);
59
+ }
60
+
61
+ on(f) { this.listeners.push(f); }
62
+ // An event made while `inside` is above nought is part of a routine's own work, as the choice
63
+ // PROB2 draws while its ask waits: the view draws it, but it is no step of the transcript.
64
+ emit(event) { if (this.inside) event.inside = true; for (const f of this.listeners) f(event); }
65
+ inside = 0;
66
+ press(key) { this.keys.push(key); }
67
+ key() { const k = this.keys.shift(); if (k !== undefined) for (const f of this.takers) f(k); return k; }
68
+ onKey(f) { this.takers.push(f); return () => { this.takers = this.takers.filter(g => g !== f); }; }
69
+
70
+ // start a script from its first step, or from a label in it; when it is loaded, as the original
71
+ // loads its file, its data goes into memory, and otherwise memory is as the game left it
72
+ enter(name, label, load = false) {
73
+ const p = this.script(name);
74
+ if (load) for (const d of p.data) this.mem.set(d.bytes, d.at);
75
+ this.program = p; this.name = name; this.pc = label === undefined ? 0 : this.label(label);
76
+ }
77
+ // a script by its name, as written (a scene, run line by line) or compiled (compile.js)
78
+ script(name) {
79
+ if (this.programs[name]) return this.programs[name];
80
+ const src = this.sources[name];
81
+ if (src === undefined) throw new Error('no script ' + name);
82
+ const p = this.programs[name] = typeof src === 'string' ? parse(src, name) : {
83
+ labels: src.labels,
84
+ data: src.data.map(([at, bytes]) => ({ at, bytes })),
85
+ steps: src.steps.map(([at, line, text], k) => ({ at, text, where: name + ':' + line, run: src.run[k], words: words(text), index: k })),
86
+ };
87
+ // a step whose label is before it starts there: the lines the lifter took into it (Engine.cost)
88
+ for (const [l, k] of Object.entries(p.labels)) { const at = /^L[0-9A-F]{4}$/.test(l) ? parseInt(l.slice(1), 16) : null; if (at !== null && p.steps[k]?.at > at) p.steps[k].start = at; if (at !== null && p.steps[k]?.at === at) p.steps[k].entered = true; }
89
+ return p;
90
+ }
91
+ // Leave everything running, the scripts called and a problem with them, for a label: as the
92
+ // original jumps to its Main Menu from the key reader, and resets the stack. The routine that
93
+ // leaves goes no further.
94
+ // A routine of the title's own code, run inside the step that calls it: its steps are part of
95
+ // that step's work, and no steps of the transcript; `done` is called as it returns.
96
+ callInside(name, label, done, ret) {
97
+ this.stack.push({ name: this.name, pc: this.pc, inside: true, done, ret });
98
+ this.inside++;
99
+ this.enter(name, label);
100
+ }
101
+ leave(name, label) {
102
+ this.stack = []; this.problem = null; this.waitingOn = null; this.visited = null;
103
+ this.enter(name, label);
104
+ throw LEFT;
105
+ }
106
+ // Nature Park's menus, from a key reader on control-E or control-T ($4C28): the routine that
107
+ // read the key asks for them and waits, so that a problem it is inside goes on living; then what
108
+ // was running is kept, with the screen, and the menus run as a routine, the A it gives them 2.
109
+ // When they return, all is put back and the routine waits for a key again, as the original's
110
+ // key reader comes back with none ($4C51). A visit from inside the menus keeps what was kept.
111
+ visit(key) { this.visiting = key; }
112
+ startVisit(here) {
113
+ const [name, label] = this.title.visit[this.visiting];
114
+ this.visiting = undefined;
115
+ if (!this.visited) {
116
+ this.visited = { name: this.name, pc: here, stack: this.stack, problem: this.problem, waitingOn: this.waitingOn, asked: this.asked, askMode: this.askMode, regs: [this.a, this.x, this.y, this.n, this.z, this.c, this.v] };
117
+ // $4C5D: the screen kept, cleared, and the graphics shown
118
+ this.title.raster?.keepScreen();
119
+ this.inside++; this.emit({ step: 'keep-screen' }); this.inside--;
120
+ this.routines.show(this, [], null);
121
+ }
122
+ this.stack = [{ visit: true }]; this.problem = null; this.waitingOn = null; this.asked = null;
123
+ this.a = 2; this.c = 0;
124
+ this.enter(name, label);
125
+ }
126
+ endVisit() {
127
+ const v = this.visited;
128
+ this.visited = null;
129
+ // $4CFB: the screen put back, and the graphics shown
130
+ this.title.raster?.putScreen();
131
+ this.inside++; this.emit({ step: 'put-screen' }); this.inside--;
132
+ this.routines.show(this, [], null);
133
+ this.enter(v.name);
134
+ Object.assign(this, { pc: v.pc, stack: v.stack, problem: v.problem, waitingOn: v.waitingOn, asked: v.asked, askMode: v.askMode });
135
+ [this.a, this.x, this.y, this.n, this.z, this.c, this.v] = v.regs;
136
+ }
137
+ label(l) { const i = this.program.labels[l]; if (i === undefined) throw new Error(this.name + ': no label ' + l); return i; }
138
+
139
+ // reading and writing an operand
140
+ place(o) {
141
+ if (o.through !== undefined) return ((this.mem[o.through] | this.mem[o.through + 1] << 8) + this.y) & 0xffff;
142
+ if (o.pair !== undefined) { const z = (o.pair + this.x) & 0xff; return this.mem[z] | this.mem[(z + 1) & 0xff] << 8; }
143
+ if (o.via !== undefined) return ((this.mem[o.via] | this.mem[o.via + 1] << 8) + (o.index ? this[o.index] : 0)) & 0xffff;
144
+ return (o.at + (o.index ? this[o.index] : 0)) & 0xffff;
145
+ }
146
+ // The I/O page, $C000 to $C0FF, goes to the title's `io`, when it has one: the keyboard and the
147
+ // soft switches that pick what the screen shows. A read of the keyboard with no key sets
148
+ // `polled`, and the run stops there for a key, as the original's loop waits on it.
149
+ // (the title's I/O page, `ioAt`: the Apple's $C000 to $C0FF unless the title says otherwise, as
150
+ // the Commodore 64's $D000 to $DFFF)
151
+ read(at) { return this.io && this.ioAt(at) ? this.io(at) & 0xff : this.mem[at]; }
152
+ write(at, v) { if (this.io && this.ioAt(at)) this.io(at, v & 0xff); else this.mem[at] = v; }
153
+ ioAt(at) { return (at & 0xff00) === 0xc000; }
154
+ get(o) {
155
+ if (o.number !== undefined) return o.number & 0xff;
156
+ if (o.reg === 'carry') return this.c;
157
+ if (o.reg) return this[o.reg];
158
+ return this.read(this.place(o));
159
+ }
160
+ // a read for a load, a compare or the like: through an index, a cycle more when the index
161
+ // carries into the next page, as the 6502 takes it
162
+ load(o) {
163
+ if (this.timed && (o.index && !o.zp || o.through !== undefined)) {
164
+ const base = o.through !== undefined ? this.mem[o.through] | this.mem[o.through + 1] << 8 : o.via !== undefined ? this.mem[o.via] | this.mem[o.via + 1] << 8 : o.at;
165
+ if ((base & 0xff) + this[o.index ?? 'y'] > 0xff) this.cycles++;
166
+ }
167
+ return this.get(o);
168
+ }
169
+ // an indexed address, for compiled code: the same cycle more
170
+ ix(base, i) { if (this.timed && (base & 0xff) + i > 0xff) this.cycles++; return (base + i) & 0xffff; }
171
+ set(o, v) {
172
+ v &= 0xff;
173
+ if (o.reg) { this[o.reg] = v; return; }
174
+ this.write(this.place(o), v);
175
+ }
176
+ flags(v) { this.n = v >> 7 & 1; this.z = v === 0 ? 1 : 0; return v; }
177
+ compare(r, v) { const d = (r - v) & 0x1ff; this.c = r >= v ? 1 : 0; this.flags(d & 0xff); }
178
+
179
+ // The statements' work, the same for a script run line by line (exec) and one compiled to
180
+ // JavaScript (compile.js).
181
+ // The cycles of the lines from step i to step j of a script, the instructions' as the original
182
+ // has them in memory at the lines' addresses, a branch taken one more (Engine.branch, and the
183
+ // compiled branches): kept with step i, the first time, and counted toward the run's delay.
184
+ time(s, i, j) {
185
+ if (!this.timed) return;
186
+ let c = s[i].cycles;
187
+ if (c === undefined) { c = 0; for (let k = i; k <= j; k++) c += this.cost(s, k); s[i].cycles = c; }
188
+ this.cycles += c; this.delay += c / CYCLES_PER_MS;
189
+ }
190
+ // A line's cycles: its instruction's, a compare and its branch both (if a != 13 goto L), and
191
+ // those the lifter left out just before it, from where the line before ended (past the data
192
+ // after its call, as the title reads it, `inline`): a register's load it took into the step's
193
+ // inputs (hposn 34 57 0 was LDA, LDX, LDY and the JSR), a NOP
194
+ cost(s, k) {
195
+ const step = s[k], m = this.mem;
196
+ if (step.at === null) return 0;
197
+ let c = CYCLES[m[step.at]] + (COMPARED(step) ? 2 : 0);
198
+ // the line before falls through to it, unless the line is where a jump comes in (its label at
199
+ // its own address) or the line before goes elsewhere
200
+ const prev = !step.entered && !['return', 'goto'].includes(s[k - 1]?.words[0]) ? s[k - 1] : null;
201
+ if (step.start !== undefined || prev && prev.at !== null) {
202
+ // from its label, where a jump comes in, or where the line before ended
203
+ const from = step.start ?? prev.at + size(m[prev.at]) + (COMPARED(prev) ? 2 : 0) + (this.routines.inline?.(this, prev) ?? 0);
204
+ if (from < step.at && step.at - from <= 9) {
205
+ let more = 0, a = from;
206
+ for (; a < step.at && LEFT_OUT.has(m[a]); a += size(m[a])) more += CYCLES[m[a]];
207
+ if (a === step.at) c += more; // only these, or the bytes between are no code (a variable)
208
+ }
209
+ }
210
+ return c;
211
+ }
212
+ // ADC: the sum with the carry into A, and the flags; in decimal mode (SED, as Zoopak's clocks
213
+ // count) by the 6502's own rules, as @moonbase-machines/mos6502/cpu.js
214
+ add(m) {
215
+ if (this.d) {
216
+ let lo = (this.a & 15) + (m & 15) + this.c, hi = (this.a >> 4) + (m >> 4);
217
+ if (lo > 9) { lo -= 10; hi++; }
218
+ this.z = ((this.a + m + this.c) & 0xff) === 0 ? 1 : 0;
219
+ this.n = hi & 8 ? 1 : 0; this.v = (~(this.a ^ m) & (this.a ^ (hi << 4)) & 0x80) ? 1 : 0;
220
+ if (hi > 9) { hi -= 10; this.c = 1; } else this.c = 0;
221
+ this.a = ((hi << 4) | lo) & 0xff; return;
222
+ }
223
+ const a = this.a, sum = a + m + this.c;
224
+ this.v = (~(a ^ m) & (a ^ sum) & 0x80) ? 1 : 0; this.c = sum > 0xff ? 1 : 0; this.a = this.flags(sum & 0xff);
225
+ }
226
+ // SBC: in binary ADC with the operand turned over; in decimal mode by the 6502's rules
227
+ sub(m) {
228
+ if (!this.d) return this.add(m ^ 0xff);
229
+ let lo = (this.a & 15) - (m & 15) - (1 - this.c), hi = (this.a >> 4) - (m >> 4);
230
+ if (lo < 0) { lo += 10; hi--; }
231
+ const r = this.a - m - (1 - this.c); this.z = (r & 0xff) === 0 ? 1 : 0; this.n = r & 0x80 ? 1 : 0;
232
+ this.v = ((this.a ^ m) & (this.a ^ r) & 0x80) ? 1 : 0; this.c = r >= 0 ? 1 : 0;
233
+ if (hi < 0) hi += 10;
234
+ this.a = ((hi << 4) | lo) & 0xff;
235
+ }
236
+ // ASL, LSR, ROL and ROR: the value shifted, the bit out into the carry
237
+ shift(v, left, through) {
238
+ const r = left ? (v << 1 | (through ? this.c : 0)) & 0xff : v >> 1 | (through && this.c ? 0x80 : 0);
239
+ this.c = left ? v >> 7 : v & 1; return this.flags(r);
240
+ }
241
+ // BIT: N and V from the value, Z from it ANDed with A
242
+ test(v) { this.n = v >> 7 & 1; this.v = v >> 6 & 1; this.z = (v & this.a) === 0 ? 1 : 0; }
243
+ // goto a label of this script's; one that a file read since has put another script's code
244
+ // over is that script's (the title's `elsewhere`)
245
+ jump(l, step) {
246
+ if (this.routines.elsewhere?.(this, l)) return this.routines['goto-engine'](this, ['$' + l.slice(1)], step);
247
+ this.pc = this.label(l);
248
+ }
249
+ // goto ($8371): through the address kept at $8371 and $8372, as JMP ($8371), into this script
250
+ // or the title's code elsewhere
251
+ jumpThrough(t, step) {
252
+ const l = label16(this.mem[t] | this.mem[t + 1] << 8);
253
+ if (this.program.labels[l] === undefined && this.routines['goto-engine']) return this.routines['goto-engine'](this, ['$' + l.slice(1)], step);
254
+ this.pc = this.label(l);
255
+ }
256
+ // goto [$8180 y]: through a table of addresses, the one at the index
257
+ jumpTable(t, i) { this.pc = this.label(label16(this.mem[t + i] | this.mem[t + i + 1] << 8)); }
258
+ call(l, step) {
259
+ if (this.routines.elsewhere?.(this, l)) return this.routines['call-engine'](this, ['$' + l.slice(1)], step);
260
+ this.stack.push({ name: this.name, pc: this.pc, ret: returnOf(step) }); this.pc = this.label(l);
261
+ }
262
+ // call ($0D19): through the address the code keeps there, into this script or another's
263
+ callThrough(t, step) {
264
+ const l = label16(this.mem[t] | this.mem[t + 1] << 8);
265
+ if (this.program.labels[l] === undefined && this.routines['call-engine']) return this.routines['call-engine'](this, ['$' + l.slice(1)], step);
266
+ this.stack.push({ name: this.name, pc: this.pc, ret: returnOf(step) }); this.pc = this.label(l);
267
+ }
268
+ ret(step) {
269
+ const f = this.stack.pop();
270
+ if (!f) throw new Error('return with nothing called');
271
+ if (f.visit) { this.endVisit(); return; }
272
+ const p = this.pulled;
273
+ // two bytes pushed, an address less one, high byte first: RTS goes there, the 6502's
274
+ // jump through a table (PHA PHA RTS, as LIB4's menu goes to its choice)
275
+ if (f.a !== undefined && this.stack.at(-1)?.a !== undefined) {
276
+ const hi = this.stack.pop().a, to = ((hi << 8 | f.a) + 1) & 0xffff, l = label16(to);
277
+ // or back past the data after a call whose frame was pulled to read it (fill's, print's):
278
+ // to the step after the call, the next in its script (which may start a little further on,
279
+ // where the lifter took a register's load into the step that uses it)
280
+ if (p?.ret !== undefined && to > p.ret && to - p.ret <= 0x100) {
281
+ this.pulled = null;
282
+ if (p.name !== this.name) this.enter(p.name);
283
+ this.pc = p.pc;
284
+ if (p.inside) { this.inside--; p.done?.(this); }
285
+ return;
286
+ }
287
+ // or back before the call, in the script that made it, to make a call again: Detect-A-Pet's
288
+ // disk error screens, which once the disk is back go back to the ProDOS call that failed
289
+ // (still inside the routine, as the original's count of calls is, until the call made
290
+ // again comes back to where the first would have: `again`)
291
+ if (p?.ret !== undefined && to <= p.ret && p.ret - to <= 0x20 && this.script(p.name).labels[l] !== undefined) {
292
+ this.pulled = null;
293
+ if (p.name !== this.name) this.enter(p.name);
294
+ this.pc = this.label(l);
295
+ if (p.inside) this.again = this.again?.ret === p.ret ? { ...this.again, n: this.again.n + 1 } : { ret: p.ret, done: p.done, n: 1 };
296
+ return;
297
+ }
298
+ if (this.program.labels[l] === undefined && this.routines['goto-engine']) return this.routines['goto-engine'](this, ['$' + l.slice(1)], step);
299
+ this.pc = this.label(l); return;
300
+ }
301
+ // a routine's frame pulled and dropped, and a return from below where it was: what it was
302
+ // inside is left now (the routine's own calls return above it)
303
+ if (p && this.stack.length < p.depth) { this.pulled = null; if (p.inside && p.ret !== undefined) this.inside--; }
304
+ if (f.name !== this.name) this.enter(f.name);
305
+ this.pc = f.pc;
306
+ // the end of a routine run inside a step (Engine.inside): what it gives, as its result; and
307
+ // what a frame does as it is returned to (`after`: BASIC's SYS keeps the registers)
308
+ if (f.inside) { this.inside--; f.done?.(this); }
309
+ if (this.again && f.ret === this.again.ret) { const a = this.again; this.again = null; this.inside -= a.n; a.done?.(this); }
310
+ f.after?.(this);
311
+ }
312
+ push(a) { this.stack.push(a ? { a: this.a } : { flags: [this.n, this.z, this.c, this.v] }); }
313
+ pull(a) {
314
+ // a call's frame is the two bytes of its return address on the original's stack, the last
315
+ // byte of its JSR: PLA gives the low byte, then the high, and two pulls drop it, as PLA PLA
316
+ // drops a return to go back two calls at once ($0D26), or as fill takes its data from after
317
+ // the call; the frame is kept (`pulled`) for the return that goes back past the data
318
+ const top = this.stack.at(-1);
319
+ if (top && top.name !== undefined) {
320
+ const v = top.ret === undefined ? 0 : top.half ? top.ret >> 8 : top.ret & 0xff;
321
+ if (top.half) { this.stack.pop(); top.depth = this.stack.length; this.pulled = top; if (top.inside && top.ret === undefined) this.inside--; } else top.half = true;
322
+ if (a) this.a = this.flags(v);
323
+ return;
324
+ }
325
+ const f = this.stack.pop(); if (a) this.a = this.flags(f.a); else [this.n, this.z, this.c, this.v] = f.flags;
326
+ }
327
+ // the machine's own instructions a scene keeps. TSX gives the stack's place, as the
328
+ // original's stack pointer would be with these frames on it, and keeps the frames under
329
+ // it; TXS goes back to the frames kept at the place it is given (Detect-A-Pet's save and
330
+ // load keep the stack page and its pointer), and to none at any other, as the town's
331
+ // resets the stack and drops every script called. SED and CLD set decimal mode, and SEI and
332
+ // CLI the mask a Commodore 64's interrupt waits on (card.js). The rest (a byte that
333
+ // is no instruction) change nothing the remake holds.
334
+ asm(op) {
335
+ if (op === 'TSX') {
336
+ const sp = 0xff - this.stack.reduce((n, f) => n + (f.name === undefined ? 1 : f.half ? 1 : 2), 0) & 0xff;
337
+ (this.stacks ??= new Map()).set(sp, this.stack.map(f => ({ ...f })));
338
+ this.x = this.flags(sp);
339
+ }
340
+ if (op === 'TXS') {
341
+ const kept = this.stacks?.get(this.x);
342
+ this.stack = kept ? kept.map(f => ({ ...f })) : []; this.visited = null; this.inside = this.stack.filter(f => f.inside).length;
343
+ }
344
+ if (op === 'CLV') this.v = 0; // as LIB4 does before a branch that always goes
345
+ if (op === 'SED') this.d = 1; if (op === 'CLD') this.d = 0; // decimal mode, for ADC and SBC
346
+ if (op === 'SEI') this.i = 1; if (op === 'CLI') this.i = 0; // interrupts masked, and not
347
+ }
348
+
349
+ // run until the title waits for a key, or `limit` steps: ten million, as Detect-A-Pet's toy
350
+ // shop turns a screen over seven times, a byte at a time, in its own lifted code
351
+ run(limit = 1e7) {
352
+ // the keyboard read with no key, in this run (`io`): a run that goes on after the time the
353
+ // original took ('pace') goes on counting, as the machine's one run would
354
+ if (!this.pacing) this.idle = 0;
355
+ this.pacing = false;
356
+ this.delay ??= 0;
357
+ for (let i = 0; i < limit; i++) {
358
+ if (this.ended) return 'end';
359
+ const step = this.program.steps[this.pc];
360
+ if (!step) throw new Error(this.name + ': ran off the end');
361
+ // the title's own word before a statement runs: a card title's tick stops there
362
+ if (this.routines.before?.(this, step) === 'wait') return 'idle';
363
+ const r = this.exec(step);
364
+ if (r === 'wait') return 'idle';
365
+ if (this.polled) { this.polled = false; return 'idle'; }
366
+ // the time the original took, a disk's read or a wait it counted out, in milliseconds: a
367
+ // frame's worth or more stops the run, for the page to show the screen and take the time
368
+ if (this.delay >= 16) { this.pacing = true; return 'pace'; }
369
+ }
370
+ return 'steps';
371
+ }
372
+
373
+ // A step: a compiled script's own function (`run`, for a run of statements together, or a
374
+ // routine's step), or the step's words, line by line. Only a routine's step that waits runs
375
+ // again when the key comes.
376
+ exec(step) {
377
+ const here = this.pc;
378
+ this.pc++;
379
+ try {
380
+ this.called = false;
381
+ if (!step.run && STATEMENT(step)) this.time(this.program.steps, here, here);
382
+ const r = step.run ? step.run(this, this.program.steps) : this.interpret(step);
383
+ if (r === 'wait' && (step.run ? this.called : !STATEMENT(step))) {
384
+ this.pc = here;
385
+ if (this.visiting !== undefined) { this.startVisit(here); return; }
386
+ }
387
+ return r;
388
+ } catch (e) {
389
+ if (e === LEFT) { this.inside = 0; return; } // counted down as it left what it was inside
390
+ // an error the title answers as its machine did (a BASIC program's, BASIC's message and the
391
+ // end of the program: card.js `error`)
392
+ if (this.routines.error?.(this, e)) { this.ended = true; return; }
393
+ e.message = step.where + ' (' + step.text + '): ' + e.message;
394
+ throw e;
395
+ }
396
+ }
397
+
398
+ interpret(step) {
399
+ const w = step.words, [first, second] = w;
400
+ // a = $8235, $866A = a, x += 1, a &= 127, $8235 <<= 1
401
+ if (second === '=' && w.length === 3) {
402
+ const o = operand(first), v = this.load(operand(w[2]));
403
+ if (o.reg === 'carry') { this.c = v & 1; return; }
404
+ this.set(o, v); if (o.reg) this.flags(v); // a load sets the flags, a store does not
405
+ return;
406
+ }
407
+ if (second === '+=' || second === '-=') {
408
+ const o = operand(first), v = w.length === 3 ? this.get(operand(w[2])) : this.load(operand(w[2]));
409
+ if (w.length === 3) { const r = (this.get(o) + (second === '+=' ? v : -v)) & 0xff; this.set(o, r); this.flags(r); return; } // increment or decrement
410
+ if (second === '+=') this.add(v); else this.sub(v); return; // with the carry, as ADC and SBC
411
+ }
412
+ if (second === '&=' || second === '|=' || second === '^=') { const v = this.load(operand(w[2])); this.a = this.flags(second === '&=' ? this.a & v : second === '|=' ? this.a | v : this.a ^ v); return; }
413
+ if (second === '<<=' || second === '>>=' || second === 'rotate') {
414
+ const o = operand(first);
415
+ this.set(o, this.shift(this.get(o), second === '<<=' || w[2] === 'left', second === 'rotate')); return;
416
+ }
417
+ switch (first) {
418
+ case 'if': return this.branch(w, step);
419
+ case 'goto':
420
+ if (w[1].startsWith('(')) return this.jumpThrough(parseInt(w[1].slice(2, -1), 16), step);
421
+ if (w[1].startsWith('[')) return this.jumpTable(parseInt(w[1].slice(2), 16), this[w[2][0]]);
422
+ return this.jump(w[1], step);
423
+ case 'call':
424
+ if (w[1].startsWith('(')) return this.callThrough(parseInt(w[1].slice(2, -1), 16), step);
425
+ return this.call(w[1], step);
426
+ case 'return': return this.ret(step);
427
+ case 'compare': this.compare(this[w[1]], this.load(operand(w[2]))); return;
428
+ case 'test': this.test(this.get(operand(w[1]))); return;
429
+ case 'asm': if (w[1] === 'RTI') return this.ret(step); this.asm(w[1]); return; // (RTI: the end of an interrupt's routine, card.js)
430
+ case 'push': this.push(w[1] === 'a'); return;
431
+ case 'pull': this.pull(w[1] === 'a'); return;
432
+ }
433
+ return this.routine(first, w.slice(1), step); // a quoted word comes as { string }
434
+ }
435
+ // a step: the title's routines do the engine's work
436
+ routine(name, args, step) {
437
+ const routine = this.routines[name];
438
+ if (!routine) throw new Error('no routine ' + name);
439
+ this.called = true;
440
+ // the call into the toolkit, its JSR's cycles; the routine's own lines count as they run
441
+ // and the loads the lifter took into the step (Engine.cost)
442
+ if (step && this.timed && step.at !== null) { const c = this.cost(this.program.steps, step.index); this.cycles += c; this.delay += c / CYCLES_PER_MS; }
443
+ return routine(this, args, step);
444
+ }
445
+
446
+ // if <flag> goto L, if not <flag> goto L, if <reg> <op> <operand> goto L
447
+ branch(w, step) {
448
+ const to = w.at(-1), cond = w.slice(1, -2);
449
+ let yes;
450
+ if (cond.length === 3 && /^[axy]$/.test(cond[0])) {
451
+ this.compare(this[cond[0]], this.load(operand(cond[2])));
452
+ yes = { '==': this.z, '!=': !this.z, '>=': this.c, '<': !this.c }[cond[1]];
453
+ } else {
454
+ const not = cond[0] === 'not', flag = not ? cond[1] : cond[0];
455
+ const v = { zero: this.z, carry: this.c, negative: this.n, overflow: this.v }[flag];
456
+ if (v === undefined) throw new Error('no flag ' + flag);
457
+ yes = not ? !v : !!v;
458
+ }
459
+ if (yes) { this.cycles += branchTaken(step); this.pc = this.label(to); }
460
+ }
461
+ }