@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/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 +81 -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
|
@@ -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
|
+
}
|
package/remake/engine.js
ADDED
|
@@ -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
|
+
}
|