@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.
@@ -0,0 +1,79 @@
1
+ // What a step is, as an event: the same words for the original, where the pilot's observer sees
2
+ // the engine's routine called, and for the remake, where a script's step runs. Two runs are the
3
+ // same when their events are.
4
+
5
+ // the registers each routine takes, in order
6
+ export const TAKES = {
7
+ box: 'axy', object: 'axy', 'object-keep': 'axy', 'object-mode': 'axy', objects: 'xy', span: 'axy', random: 'a', beep: 'a',
8
+ 'key-at': 'axy', key: 'a', multiply: 'ax', divide: 'axy', ask: 'a', 'ask-mode': 'a', 'ask-escape': 'a', rectangle: 'axy', 'read-dot': 'xy',
9
+ problem: 'a', dot: 'axy', 'fill-rows': 'axy', part: 'a', 'rectangle-outline': 'axy', 'key-panel': 'a', 'keep-span': 'xy',
10
+ 'put-span': 'xy', 'white-lines': 'y', 'invert-span': 'xy',
11
+ // Animal Rescue's problems: PROB1, the chimps' tree, a box by its letter
12
+ label: 'a', 'flip-box': 'a', 'flash-box': 'a',
13
+ // PROB2, the elephants: a mark on the path by its place, and a jump by how far it goes
14
+ path: 'ax', jump: 'a',
15
+ // PROB3, the parrots' food: a box of the grid by its column and row, what shows in it, and a pair
16
+ cell: 'axy', 'cell-box': 'axy', pair: 'a',
17
+ // PROB4, the puzzle box: a piece by its edges, where it goes, and its color in $11
18
+ piece: 'axy',
19
+ // Nature Park's PROB1, the colour pattern: a shape coloured in with a pen, the shape in $0E15
20
+ colour: 'a',
21
+ // PROB3, the truck: a turn of its loop, which gives the key down then, or 0
22
+ tick: '',
23
+ // Detect-A-Pet's card toolkit (its title's map): print takes its x in X and Y, its y in A, and
24
+ // the carry for XDRAW; HPOSN its y in A and x in X and Y; HLIN its x in A and X and y in Y
25
+ print: 'axyc', arrow: 'a', unpack: 'y', 'load-picture': 'a', 'action-cards': 'a', 'return-key': 'a', 'space-key': 'a',
26
+ 'wipe-rows': 'a', 'wipe-columns': 'a', 'text-card': 'a', 'fill-left': 'a', 'put-object': 'axy', 'draw-room-objects': 'yc', take: 'y',
27
+ drop: 'y', effect: 'a', outport: 'a', cout: 'a',
28
+ // Pieces of Eight's text: a character at the cursor, a message by its number, and a room's
29
+ // description from one of three tables
30
+ char: 'a', message: 'xy', describe: 'axy',
31
+ // the Commodore 64's KERNAL (Zoopak): a character out, the cursor's place, a file's number,
32
+ // device and secondary address, its name, a load or save, and the serial bus's bytes
33
+ chrout: 'a', plot: 'xyc', close: 'a', chkin: 'x', chkout: 'x', setlfs: 'axy', setnam: 'axy', load: 'axy', save: 'axy', listen: 'a', second: 'a', ciout: 'a', talk: 'a', tksa: 'a', setmsg: 'a', tone: 'xy', 'load-objects': 'axy', hposn: 'axy', hlin: 'axy', hcolor: 'x', pause: 'a', 'bag-put': 'a',
34
+ };
35
+
36
+ // the inputs some problems' routines take from their own memory, at fixed addresses
37
+ const MEMORY = { colour: { shape: 0x0e15 }, 'card-frame': { x: 0x4078, high: 0x4079, y: 0x407a, width: 0x407b, height: 0x407c } };
38
+
39
+ // the inputs some routines take from the zero page rather than the registers: the line's ends
40
+ // and color, and a rectangle's width and height
41
+ const ZERO = { line: { x0: 0x00, x1: 0x01, y0: 0x02, y1: 0x03, color: 0x11 }, piece: { color: 0x11 }, rectangle: { width: 0x0e, height: 0x0f }, 'rectangle-outline': { width: 0x0e, height: 0x0f } };
42
+
43
+ // A step as an event: its name, any fixed input (a panel's number), the registers it takes,
44
+ // the zero page inputs, read through `read`, and the data it had inline (a text, a picture's
45
+ // name, a sound).
46
+ export function describe(name, fixed, regs, inline, read, peek) {
47
+ const e = { step: name };
48
+ if (fixed.length) e.n = fixed.map(Number);
49
+ const takes = TAKES[name];
50
+ if (name === 'objects') e.table = '$' + ((regs.x | regs.y << 8).toString(16).toUpperCase().padStart(4, '0'));
51
+ // object 0 or 1 puts back the ground kept under an object, where it was: what is left in X and Y
52
+ // then is whatever the steps before left there, and is no input
53
+ else if (name.startsWith('object') && regs.a < 2) e.a = regs.a;
54
+ else if (takes) for (const r of takes) e[r] = regs[r];
55
+ if ((name === 'key-at' || name === 'key') && e.a !== undefined) e.a = String.fromCharCode(e.a & 0x7f);
56
+ if (ZERO[name] && read) for (const [k, at] of Object.entries(ZERO[name])) e[k] = read(at);
57
+ if (MEMORY[name] && peek) for (const [k, at] of Object.entries(MEMORY[name])) e[k] = peek(at);
58
+ if (inline !== undefined) e.words = inline;
59
+ return e;
60
+ }
61
+
62
+ // the outputs of a routine that a scene goes on to use, recorded as it returns
63
+ export const GIVES = { random: 'a', ask: 'ax', 'ask-mode': 'ax', 'ask-escape': 'axc', 'wait-or-escape': 'c', problem: 'c', 'read-key': 'a', 'read-dot': 'a', multiply: 'ax', divide: 'ax', 'blink-key': 'a', 'which-box': 'xy', tick: 'a', 'choose-action': 'a', take: 'a', drop: 'a', 'show-bag': 'a', 'bag-choose': 'a', getin: 'a', acptr: 'a', readst: 'a', load: 'cxy', chrin: 'a' };
64
+
65
+ // A call that does nothing is no step: the beep returns at once for A 0 ($6831 in Animal Rescue,
66
+ // $5D12 in Nature Park), and a scene that reads keys by polling beeps each one, 0 for none, until
67
+ // one comes. As steps those would be as many as the polls, which is how long the machine was
68
+ // watched, not what the title did.
69
+ export const nothing = (name, a) => name === 'beep' && a === 0;
70
+
71
+ // routines that poll for a key: each is a step only when it gives one
72
+ export const POLLS = new Set(['read-key', 'blink-key']);
73
+ // routines that wait for keys; any other, while it runs, is busy, and a key pressed then is not for it
74
+ export const WAITS = new Set(['wait', 'wait-erase', 'wait-or-escape', 'ask', 'ask-mode', 'ask-escape', 'which-box', 'tick', 'choose-action', 'take', 'drop', 'show-bag', 'bag-choose']);
75
+ export function result(name, regs) {
76
+ const e = { result: name };
77
+ for (const r of GIVES[name]) e[r] = regs[r];
78
+ return e;
79
+ }
@@ -0,0 +1,144 @@
1
+ // A text laid out as the engine's text routine ($6CE2) lays it out, for the view to draw and for
2
+ // the screen model to paint. `pen` is the text state that lasts from text to text: the pen's x and
3
+ // y, the margin, centring, the spacing, the gap between lines, the ink and the paper. It is changed
4
+ // here as the engine changes it.
5
+ //
6
+ // {0:x} the pen's x and the margin {1:y} its y, or from $C0 the line height
7
+ // {2:n} the spacing {3:n} the gap between lines {4:c} the ink {5:c} the paper
8
+ // {7} and {8} centring on and off {9} and {a} tall on and off {f:ch,n} a character n times
9
+ // {d} and {e} ink 0 and ink 1 from here on {10} and {11} the big digits and back
10
+ //
11
+ // Returns the lines, each { top, tall, height, paper, ink, chars }, and each character { ch, x, ink },
12
+ // its x in dots. A character's ink is undefined before the line's first color: the engine draws
13
+ // that part in the ink the whole line ends with, which is the line's `ink`. A byte below a space
14
+ // is a spacer, a few dots of room and nothing drawn.
15
+ import { width, start } from '@moonbase-machines/vector/font.js';
16
+
17
+ // the screen's layout by $03E3: the window's last row, and the tops of the two boxes below it
18
+ export const LAYOUT = { 0x41: [116, 116, 174], 0x32: [116, 116, 160], 0x31: [130, 130, 174], 0x22: [130, 130, 160], 0x11: [158, 158, 174] };
19
+ // the panels: the layout they set, the rows of the box they clear, from its two white rows down,
20
+ // and its color, and the text state they leave; `more`, white rows drawn below as well. 1 to 6 are
21
+ // Animal Rescue's, $6299 to $6395; Nature Park has the same six and eighteen more, $5712 to $58A6,
22
+ // in families that share a body and differ in the gap and the row they leave (its title's map).
23
+ const gray = (layout, rows, gap, y, more) => ({ layout, rows, fill: 10, paper: 10, ink: 0, gap, y, more });
24
+ const blue = (layout, rows, gap, y) => ({ layout, rows, fill: 5, paper: 5, ink: 15, gap, y });
25
+ export const PANELS = {
26
+ 1: gray(0x32, [116, 159], 7, 128), 2: gray(0x32, [116, 159], 33, 135),
27
+ 3: gray(0x22, [130, 159], 5, 135), 4: gray(0x22, [130, 159], 19, 142),
28
+ 5: blue(0x22, [160, 189], 5, 166), 6: blue(0x22, [160, 189], 19, 172),
29
+ 7: gray(0x41, [116, 173], 5, 121), 8: gray(0x41, [116, 173], 6, 128), 9: gray(0x41, [116, 173], 7, 134), 10: gray(0x41, [116, 173], 47, 142),
30
+ 11: gray(0x32, [116, 159], 5, 121),
31
+ 12: gray(0x31, [130, 173], 5, 135), 13: gray(0x31, [130, 173], 7, 142), 14: gray(0x31, [130, 173], 33, 149),
32
+ 15: gray(0x11, [158, 173], 5, 163, [174]),
33
+ 16: blue(0x32, [160, 189], 19, 172), 17: blue(0x42, [160, 189], 19, 172),
34
+ 18: blue(0x32, [160, 189], 5, 166), 19: blue(0x42, [160, 189], 5, 166),
35
+ 20: blue(0x23, [160, 189], 0, 163), 21: blue(0x33, [160, 189], 0, 163), 22: blue(0x43, [160, 189], 0, 163),
36
+ 23: blue(0x31, [174, 189], 5, 179), 24: blue(0x41, [174, 189], 5, 179),
37
+ };
38
+
39
+ // code 6: a word from a table of words, each its length then its letters, the index given or
40
+ // read from memory
41
+ export function word(mem, [lo, hi, vlo, vhi]) {
42
+ let at = lo | hi << 8, n = vhi === 0xff ? vlo : mem[vlo | vhi << 8];
43
+ while (n-- > 0) at += mem[at] + 1;
44
+ let s = ''; for (let k = 1; k <= mem[at]; k++) s += String.fromCharCode(mem[at + k] & 0x7f);
45
+ return s;
46
+ }
47
+ // a text's words with the words code 6 takes put in, from memory as it is now
48
+ export const resolve = (words, mem) => words.replace(/\{6:([0-9a-f,]+)\}/g, (all, b) => word(mem, b.split(',').map(v => parseInt(v, 16))));
49
+
50
+ export function layout(words, pen) {
51
+ const t = pen, lines = [];
52
+ t.tall = false; t.margin = t.x; let height = 9, big = false;
53
+ let line = [], ink = t.ink, marked = false;
54
+ const flush = newline => {
55
+ if (line.length) {
56
+ let x = start(line.map(c => c.ch), t.spacing, t.x, t.centre);
57
+ const chars = [];
58
+ for (const c of line) {
59
+ const w = width(c.ch);
60
+ chars.push({ ch: c.ch, x, ink: c.ink });
61
+ x += c.ch.charCodeAt(0) < 0x20 ? w : w + t.spacing;
62
+ }
63
+ lines.push({ top: t.y, tall: t.tall, height, paper: t.paper, ink: t.ink, chars });
64
+ t.x = x;
65
+ }
66
+ line = [];
67
+ if (newline) { t.y += (t.tall ? 2 * height : height) + t.gap; t.x = t.margin; }
68
+ };
69
+ // a digit in the big set is its own character, $80 and on (@moonbase-machines/vector/font.js)
70
+ const put = ch => line.push({ ch: big && ch >= '0' && ch <= '9' ? String.fromCharCode(0x80 + ch.charCodeAt(0) - 0x30) : ch, ink: marked ? ink : undefined });
71
+ for (let i = 0; i < words.length; i++) {
72
+ const c = words[i];
73
+ if (c === '\n') { flush(true); continue; }
74
+ if (c === '{') {
75
+ const end = words.indexOf('}', i), [code, rest] = words.slice(i + 1, end).split(':'), b = (rest ?? '').split(',').map(v => parseInt(v, 16));
76
+ i = end;
77
+ switch (parseInt(code, 16)) {
78
+ case 0: t.x = t.margin = b[0]; break;
79
+ case 1: if (b[0] < 0xc0) t.y = b[0]; else height = b[0] - 0xc0; break;
80
+ case 2: t.spacing = b[0]; break;
81
+ case 3: t.gap = b[0]; break;
82
+ case 4: t.ink = ink = b[0]; marked = true; break;
83
+ case 5: t.paper = b[0]; break;
84
+ case 7: t.centre = true; break;
85
+ case 8: t.centre = false; break;
86
+ case 9: t.tall = true; break;
87
+ case 10: t.tall = false; break;
88
+ case 13: ink = 0; marked = true; break;
89
+ case 14: ink = 1; marked = true; break;
90
+ case 16: big = true; break;
91
+ case 17: big = false; break;
92
+ case 15: for (let k = 0; k < (b[1] || 256); k++) put(String.fromCharCode(b[0] & 0x7f)); break;
93
+ }
94
+ continue;
95
+ }
96
+ put(c);
97
+ }
98
+ flush(false);
99
+ return lines;
100
+ }
101
+
102
+ // The same codes on the text screen, where the menus and the teacher's controls are ($573D puts
103
+ // the machine in 40 columns of text): {0:x} is the column and the margin, {1:y} the row, a new
104
+ // line the next row at the margin, {7} centres each line in the 40 columns, and {d} and {e} turn
105
+ // inverse on and off. Inverse has capitals only, as the text page's inverse codes do. The
106
+ // spacing, the gap, the colors and tall mean nothing there.
107
+ // Returns the characters as { row, col, ch, inverse }, and leaves the pen where the text ended.
108
+ export function textLayout(words, pen) {
109
+ const cells = [];
110
+ pen.inverse = false; // each text starts out of inverse ($6D02)
111
+ let line = [];
112
+ const flush = newline => {
113
+ const col = pen.centre ? (40 - line.length) >> 1 : pen.x;
114
+ line.forEach((c, k) => cells.push({ row: pen.y, col: col + k, ch: c.ch, inverse: c.inverse }));
115
+ if (line.length) pen.x = col + line.length;
116
+ line = [];
117
+ if (newline) { pen.y++; pen.x = pen.margin; }
118
+ };
119
+ const put = ch => line.push({ ch: pen.inverse ? ch.toUpperCase() : ch, inverse: pen.inverse });
120
+ for (let i = 0; i < words.length; i++) {
121
+ const c = words[i];
122
+ if (c === '\n') { flush(true); continue; }
123
+ if (c === '{') {
124
+ const end = words.indexOf('}', i), [code, rest] = words.slice(i + 1, end).split(':'), b = (rest ?? '').split(',').map(v => parseInt(v, 16));
125
+ i = end;
126
+ switch (parseInt(code, 16)) {
127
+ case 0: flush(false); pen.x = pen.margin = b[0]; break;
128
+ case 1: flush(false); if (b[0] < 0xc0) pen.y = b[0]; break;
129
+ case 7: pen.centre = true; break;
130
+ case 8: pen.centre = false; break;
131
+ case 13: pen.inverse = true; break;
132
+ case 14: pen.inverse = false; break;
133
+ case 15: for (let k = 0; k < (b[1] || 256); k++) put(String.fromCharCode(b[0] & 0x7f)); break;
134
+ }
135
+ continue;
136
+ }
137
+ put(c);
138
+ }
139
+ flush(false);
140
+ return cells;
141
+ }
142
+ // the text state at the start, and the empty screen
143
+ export const textPen = () => ({ x: 0, y: 0, margin: 0, centre: false, inverse: false });
144
+ export const textPage = () => Array.from({ length: 24 }, () => Array.from({ length: 40 }, () => ({ ch: ' ', inverse: false })));
package/remake/lift.js ADDED
@@ -0,0 +1,197 @@
1
+ // The lifter: a file's 6502 walked from its entries into a scene script, a step to a line
2
+ // (a tool prints them; the remake lifts at need in the browser, card.js).
3
+ import { OPS, SIZE, operand, hex } from '@moonbase-machines/mos6502/disasm.js';
4
+ import { INLINE as READERS } from '@moonbase-machines/vector/inline.js';
5
+ import { TAKES } from './events.js';
6
+
7
+ // the data after a call, read from the file, with the text quoted so that it reads as one word
8
+ export const readers = from => Object.fromEntries(Object.entries(from).map(([k, f]) => [k, (b, at) => { const r = f(i => b[i], at); return { length: r.length, words: k === 'text' || k === 'print' || k === 'say' ? JSON.stringify(r.value) : r.value }; }]));
9
+ const INLINE = readers(READERS);
10
+
11
+ // `stops` are addresses where data begins that is not a table of code addresses, the objects'
12
+ // tables and the objects themselves, so that a table of addresses read from the code ends there.
13
+ export function lift(bytes, origin, entries, routines, stops = new Set(), inline = INLINE) {
14
+ const INLINE = inline;
15
+ // `seen`: every byte an instruction or its data takes, for what is left as data; an instruction
16
+ // is walked from where it starts, which may be inside another's bytes (the BIT that skips)
17
+ const lines = new Map(), targets = new Set(entries), seen = new Set(), queue = [...entries];
18
+ const inFile = a => a >= origin && a < origin + bytes.length;
19
+ while (queue.length) {
20
+ let pc = queue.pop();
21
+ const regs = {};
22
+ while (inFile(pc) && !lines.has(pc)) {
23
+ seen.add(pc);
24
+ const at = pc - origin, op = OPS[bytes[at]];
25
+ if (op.name === '???') { lines.set(pc, { line: '.byte $' + hex(bytes[at], 2) + ' ; not an instruction', end: pc + 1 }); break; }
26
+ const size = SIZE[op.mode], word = bytes[at + 1] | bytes[at + 2] << 8, ins = bytes.subarray(at, at + size);
27
+ let line = (op.name + ' ' + operand(op.mode, ins, pc)).trim(), next = pc + size, stop = false;
28
+ if (op.mode === 'imm' && 'LDA LDX LDY'.includes(op.name)) regs[op.name[2]] = bytes[at + 1];
29
+ else if (op.name.startsWith('T') || op.name.startsWith('IN') || op.name.startsWith('DE') || op.name.startsWith('LD') || op.name === 'PLA') delete regs[op.name.at(-1)];
30
+ if (op.name === 'JSR' && routines[word]) {
31
+ const r = routines[word], data = INLINE[r]?.(bytes, next - origin);
32
+ const args = ['A', 'X', 'Y'].map(k => regs[k] === undefined ? '' : k + '=' + regs[k]).filter(Boolean).join(' ');
33
+ line = r + (data ? ' ' + data.words : '') + (args && !data ? ' ; ' + args : '');
34
+ if (data) next += data.length;
35
+ for (const k in regs) delete regs[k];
36
+ } else if (op.name === 'JSR') { if (inFile(word)) { targets.add(word); queue.push(word); } for (const k in regs) delete regs[k]; }
37
+ else if (op.name === 'JMP' && op.mode === 'abs') { if (inFile(word)) { targets.add(word); queue.push(word); } else line += ' ; into PART0' + (routines[word] ? ', ' + routines[word] : ''); stop = true; }
38
+ else if (op.name === 'JMP' || op.name === 'RTS' || op.name === 'RTI' || op.name === 'BRK') stop = true;
39
+ else if (op.mode === 'rel') { const t = (pc + 2 + (bytes[at + 1] << 24 >> 24)) & 0xffff; targets.add(t); queue.push(t); }
40
+ // a table of addresses, read low byte then high byte, LDA t,X or t,Y and then LDA t+1 the same
41
+ // way within a few instructions: every address in it that is in the file is code to follow
42
+ if ((op.mode === 'abx' || op.mode === 'aby') && op.name === 'LDA' && inFile(word)) {
43
+ for (let k = at + 3; k < at + 12 && k + 2 < bytes.length; k++) {
44
+ if (bytes[k] !== bytes[at] || (bytes[k + 1] | bytes[k + 2] << 8) !== word + 1) continue;
45
+ const table = [];
46
+ for (let e = word - origin; e + 1 < bytes.length && table.length < 64; e += 2) { if (table.length && stops.has(origin + e)) break; const t = bytes[e] | bytes[e + 1] << 8; if (!inFile(t)) break; table.push(t); }
47
+ line += ' ; a table of ' + table.length + ' addresses';
48
+ for (const t of table) { targets.add(t); queue.push(t); }
49
+ break;
50
+ }
51
+ }
52
+ lines.set(pc, { line, end: next, op: op.name, mode: op.mode, value: size === 2 ? bytes[at + 1] : word, call: op.name === 'JSR' && routines[word] ? { name: routines[word], words: INLINE[routines[word]]?.(bytes, pc + 3 - origin)?.words } : null, stop });
53
+ for (let a = pc + 1; a < next; a++) seen.add(a);
54
+ if (stop) break;
55
+ pc = next;
56
+ }
57
+ }
58
+ const out = [];
59
+ let last = null;
60
+ for (const pc of [...lines.keys()].sort((a, b) => a - b)) {
61
+ if (last !== null && pc !== last) out.push('');
62
+ if (targets.has(pc)) out.push('L' + hex(pc, 4) + ':');
63
+ out.push(hex(pc, 4) + ' ' + lines.get(pc).line);
64
+ last = lines.get(pc).end;
65
+ }
66
+ // what the walk never reached: tables, data, and code reached only through a table
67
+ const left = [];
68
+ for (let a = origin; a < origin + bytes.length; ) { if (seen.has(a)) { a++; continue; } const s = a; while (a < origin + bytes.length && !seen.has(a)) a++; left.push([s, a - s]); }
69
+ return { listing: out.join('\n'), left, reached: seen.size, size: bytes.length, lines, targets };
70
+ }
71
+
72
+ // The lifted code as a scene script, a step to a line, each line starting with the address it
73
+ // came from. A call into the engine is a step with its inputs, the loads before it folded in.
74
+ // Everything else is a statement on the registers a, x and y, the flags, and memory, written
75
+ // $866A for an address, $86E4[y] for one indexed, and ($12)[y] for one through the zero page.
76
+ // Each statement sets the flags as its instruction did, so a branch means what it meant:
77
+ // a compare and the branch after it are one `if a == 1 goto L8157`, and a branch alone tests a
78
+ // flag, `if zero goto L8157`. A jump through a table that the code writes into its own JMP is
79
+ // `goto [$86EC y]`. The bytes the walk never reached are kept as `data` lines, up to where the
80
+ // objects' tables begin: the objects come from those tables instead.
81
+ export function script({ lines, targets, left }, bytes, origin, { stops = new Set(), selfModified = false, memory = false, moved = [] } = {}) {
82
+ const H = (n, w = 4) => '$' + hex(n, w);
83
+ // With `selfModified`, an instruction whose operand the code writes as it runs takes it from
84
+ // memory: an immediate is the byte at its own address, an address is [$0D19], the one the code
85
+ // keeps there, and a jump or a call goes through it. The places written are every address a
86
+ // store, an increment or a shift names; and `moved`, the bytes of instructions seen to change
87
+ // as the original ran (as the pilots find them), however the code wrote them (through a pointer, as
88
+ // Find Me does)
89
+ const written = new Set(moved);
90
+ if (selfModified) for (const r of lines.values()) if (/^(STA|STX|STY|INC|DEC|ASL|LSR|ROL|ROR)$/.test(r.op) && /^(abs|abx|aby)$/.test(r.mode)) written.add(r.value);
91
+ let here = null; // the address of the instruction being written
92
+ // `memory`: code that other files write into, as the variables' file LIB.VARS, whose every
93
+ // instruction takes its operand from memory
94
+ const own = () => here !== null && (memory || written.has(here + 1) || written.has(here + 2));
95
+ const place = r => {
96
+ const v = r.value;
97
+ if (own() && r.mode === 'imm' && (memory || written.has(here + 1))) return H(here + 1);
98
+ if (own() && /^(abs|abx|aby)$/.test(r.mode)) return '[' + H(here + 1) + ']' + (r.mode === 'abs' ? '' : '[' + r.mode.at(-1) + ']');
99
+ switch (r.mode) {
100
+ case 'imm': return String(v);
101
+ case 'zp': return H(v, 2);
102
+ case 'zpx': return H(v, 2) + '[x]';
103
+ case 'zpy': return H(v, 2) + '[y]';
104
+ case 'abs': return H(v);
105
+ case 'abx': return H(v) + '[x]';
106
+ case 'aby': return H(v) + '[y]';
107
+ case 'izy': return '(' + H(v, 2) + ')[y]';
108
+ case 'izx': return '(' + H(v, 2) + '[x])';
109
+ case 'acc': return 'a';
110
+ default: return '?';
111
+ }
112
+ };
113
+ const label = a => 'L' + hex(a, 4);
114
+ const branch = { BEQ: 'zero', BNE: 'not zero', BCS: 'carry', BCC: 'not carry', BMI: 'negative', BPL: 'not negative', BVS: 'overflow', BVC: 'not overflow' };
115
+ const fused = { BEQ: '==', BNE: '!=', BCS: '>=', BCC: '<' };
116
+ const target = (pc, r) => (pc + 2 + (r.value << 24 >> 24)) & 0xffff;
117
+ const pcs = [...lines.keys()].sort((a, b) => a - b), out = [];
118
+ // An instruction that runs on into the bytes of another, as BIT $01A9 runs over the LDA #1 that
119
+ // a jump to its second byte finds: the next line is not where it goes on, so a goto says where.
120
+ const stopsHere = r => ['JMP', 'RTS', 'RTI'].includes(r.op);
121
+ targets = new Set(targets);
122
+ pcs.forEach((pc, i) => { const r = lines.get(pc); if (r.op && !stopsHere(r) && i + 1 < pcs.length && r.end !== pcs[i + 1] && lines.has(r.end)) targets.add(r.end); });
123
+ let loads = []; // immediate loads waiting to be folded into the next call
124
+ const flush = () => { for (const [pc, reg, v] of loads) out.push(hex(pc, 4) + ' ' + reg + ' = ' + v); loads = []; };
125
+ let last = null; // the instruction before, and where it goes on
126
+ for (let i = 0; i < pcs.length; i++) {
127
+ const pc = pcs[i], r = lines.get(pc), next = lines.get(pcs[i + 1]), put = s => out.push(hex(pc, 4) + ' ' + s);
128
+ here = pc;
129
+ if (last && last.op && !stopsHere(last) && last.end !== pc && lines.has(last.end)) { flush(); out.push(hex(last.pc, 4) + ' goto ' + label(last.end)); }
130
+ last = { ...r, pc: pcs[i] };
131
+ if (targets.has(pc)) { flush(); out.push(label(pc) + ':'); }
132
+ if (!r.op) { flush(); put('asm ' + r.line); continue; }
133
+ const reg = r.op.at(-1).toLowerCase();
134
+ if (r.call) {
135
+ const [name, ...fixed] = r.call.name.split(' ');
136
+ const takes = TAKES[name] || '';
137
+ const given = Object.fromEntries(loads.map(([, g, v]) => [g, v]));
138
+ // loads the call does not take stay as statements, before it
139
+ const kept = loads.filter(([, g]) => !takes.includes(g)); loads = kept; flush();
140
+ const args = [...fixed, ...[...takes].map(g => given[g] ?? g)];
141
+ if (name === 'objects' && given.x !== undefined && given.y !== undefined) args.splice(fixed.length, 2, H(given.x | given.y << 8));
142
+ if (name === 'key-at' || name === 'key') args[0] = given.a !== undefined ? JSON.stringify(String.fromCharCode(given.a & 0x7f)) : args[0];
143
+ put([name, ...args, r.call.words ?? ''].join(' ').trim());
144
+ continue;
145
+ }
146
+ switch (r.op) {
147
+ case 'LDA': case 'LDX': case 'LDY':
148
+ if (r.mode === 'imm' && !own()) { loads = loads.filter(([, g]) => g !== reg); loads.push([pc, reg, r.value]); break; }
149
+ // the jump through a table: LDA T,Y STA J+1 LDA T+1,Y STA J+2 J: JMP
150
+ if (r.mode === 'aby' || r.mode === 'abx') {
151
+ const [s1, l2, s2, j] = [1, 2, 3, 4].map(k => lines.get(pcs[i + k]));
152
+ if (s1?.op === 'STA' && l2?.op === 'LDA' && l2.value === r.value + 1 && s2?.op === 'STA' && j?.op === 'JMP' && s1.value === pcs[i + 4] + 1 && s2.value === pcs[i + 4] + 2) {
153
+ flush(); put('goto [' + H(r.value) + ' ' + r.mode.at(-1) + ']'); i += 4; last = null; break;
154
+ }
155
+ }
156
+ flush(); put(reg + ' = ' + place(r)); break;
157
+ case 'STA': case 'STX': case 'STY': flush(); put(place(r) + ' = ' + reg); break;
158
+ case 'TAX': case 'TAY': case 'TXA': case 'TYA': flush(); put(r.op[2].toLowerCase() + ' = ' + r.op[1].toLowerCase()); break;
159
+ case 'INX': case 'INY': case 'DEX': case 'DEY': flush(); put(reg + (r.op[0] === 'I' ? ' += 1' : ' -= 1')); break;
160
+ case 'INC': case 'DEC': flush(); put(place(r) + (r.op === 'INC' ? ' += 1' : ' -= 1')); break;
161
+ case 'CLC': case 'SEC': flush(); put('carry = ' + (r.op === 'SEC' ? 1 : 0)); break;
162
+ case 'ADC': flush(); put('a += ' + place(r) + ' + carry'); break;
163
+ case 'SBC': flush(); put('a -= ' + place(r) + ' + borrow'); break;
164
+ case 'AND': flush(); put('a &= ' + place(r)); break;
165
+ case 'ORA': flush(); put('a |= ' + place(r)); break;
166
+ case 'EOR': flush(); put('a ^= ' + place(r)); break;
167
+ case 'ASL': case 'LSR': case 'ROL': case 'ROR': flush(); put(place(r) + ' ' + { ASL: '<<= 1', LSR: '>>= 1', ROL: 'rotate left', ROR: 'rotate right' }[r.op]); break;
168
+ case 'BIT': flush(); put('test ' + place(r)); break;
169
+ case 'CMP': case 'CPX': case 'CPY': {
170
+ flush();
171
+ const who = r.op === 'CMP' ? 'a' : r.op[2].toLowerCase();
172
+ if (next && fused[next.op] && !targets.has(pcs[i + 1])) { put('if ' + who + ' ' + fused[next.op] + ' ' + place(r) + ' goto ' + label(target(pcs[i + 1], next))); i++; last = { ...next, pc: pcs[i] }; }
173
+ else put('compare ' + who + ' ' + place(r));
174
+ break;
175
+ }
176
+ case 'JMP': flush(); if (r.mode === 'abs' && own()) { put('goto (' + H(pc + 1) + ')'); break; } put(r.mode === 'abs' ? (r.value >= origin && r.value < origin + bytes.length ? 'goto ' + label(r.value) : 'goto-engine ' + H(r.value)) : 'goto (' + H(r.value) + ')'); break;
177
+ case 'JSR': flush(); if (own()) { put('call (' + H(pc + 1) + ')'); break; } put(r.value >= origin && r.value < origin + bytes.length ? 'call ' + label(r.value) : 'call-engine ' + H(r.value)); break;
178
+ case 'RTS': flush(); put('return'); break;
179
+ case 'PHA': case 'PLA': case 'PHP': case 'PLP': flush(); put({ PHA: 'push a', PLA: 'pull a', PHP: 'push flags', PLP: 'pull flags' }[r.op]); break;
180
+ case 'NOP': break;
181
+ default:
182
+ if (branch[r.op]) { flush(); put('if ' + branch[r.op] + ' goto ' + label(target(pc, r))); break; }
183
+ flush(); put('asm ' + r.line);
184
+ }
185
+ }
186
+ flush();
187
+ // each run of bytes the walk never reached, up to where the objects begin
188
+ for (const [a, whole] of left) {
189
+ let n = 0; while (n < whole && !(n && stops.has(a + n)) && !stops.has(a)) n++;
190
+ if (!n) continue;
191
+ const row = [];
192
+ for (let k = 0; k < n; k += 16) row.push('data ' + H(a + k) + ' ' + Array.from(bytes.subarray(a - origin + k, a - origin + Math.min(n, k + 16)), b => hex(b, 2)).join(' '));
193
+ out.push('', ...row);
194
+ }
195
+ return out.join('\n') + '\n';
196
+ }
197
+
package/remake/load.js ADDED
@@ -0,0 +1,55 @@
1
+ // One way to start a title's remake, for the remake's page (main.js), a replay and Node (a pilot's observer): the
2
+ // title's title.js, from the folder it is in, with the modules it names (its problems, its paint); its scripts,
3
+ // compiled to JavaScript (bin/moonbase-compile.mjs) or, with `scenes`, the scene files themselves to run line by line;
4
+ // its game's data, from where the page keeps it; and an engine made from them.
5
+ //
6
+ // const { title, scripts, disk, folder } = await loadTitle(folder, { data })
7
+ // folder the title's folder, an absolute URL (its title.js, its scenes, compiled/, its own modules)
8
+ // data its game's data, an absolute URL (<slug>.json: its disks' files, its kind, its saves)
9
+ // title.disk = disk; title.seed = 17 (the caller's: a data disk, the seed)
10
+ // const engine = makeEngine(title, scripts)
11
+ // With `fixed`, a title that has a version with its bugs fixed (its title.js `fixed`, a script for each it replaces)
12
+ // runs those scripts in place of its own. A title whose engine is its own (its title.js `engine` the name of a module
13
+ // in its folder, not 'card' or 'vector') has no scripts here: its engine is loaded with its other modules, as
14
+ // `title.own`, and runs the game itself (main.js).
15
+ import { Engine } from './engine.js';
16
+ import { vectorRoutines } from './vector.js';
17
+ import { cardRoutines } from './card.js';
18
+ import { started } from '@moonbase-machines/apple2e/prodos.js';
19
+
20
+ const ENGINES = { vector: vectorRoutines, card: cardRoutines };
21
+
22
+ // a file read in the browser by fetch, and in Node from the disk (Node's own fs, asked of Node itself, so that the
23
+ // browser's files import nothing of Node's)
24
+ async function read(url, as) {
25
+ if (url.protocol === 'file:') { const text = globalThis.process.getBuiltinModule('node:fs').readFileSync(url, 'utf8'); return as === 'json' ? JSON.parse(text) : text; }
26
+ const r = await fetch(url);
27
+ if (!r.ok) throw new Error(`${url.pathname}: ${r.status}`);
28
+ return as === 'json' ? r.json() : r.text();
29
+ }
30
+ const defaultOf = async url => (await import(url.href)).default;
31
+
32
+ export async function loadTitle(folder, { data, scenes = false, fixed = false } = {}) {
33
+ folder = new URL(folder);
34
+ const title = { ...(await defaultOf(new URL('title.js', folder))) };
35
+ // the modules a title has of its own, as its title.js names them (problems.js, paint.js), and its view's own
36
+ // drawing (view.js), where it says it has one
37
+ for (const name of title.modules ?? []) title[name] = await defaultOf(new URL(name + '.js', folder));
38
+ if (title.view) title.viewSteps = await defaultOf(new URL('view.js', folder));
39
+ if (!ENGINES[title.engine]) {
40
+ title.own = await import(new URL(title.engine + '.js', folder).href);
41
+ return { title, scripts: {}, disk: null, folder };
42
+ }
43
+ const scripts = Object.fromEntries(await Promise.all(title.scripts.map(async n =>
44
+ [n, scenes ? await read(new URL(n + '.scene', folder), 'text') : await defaultOf(new URL(`compiled/${(fixed && title.fixed?.[n]) || n}.js`, folder))])));
45
+ const disk = await read(new URL(data), 'json');
46
+ return { title, scripts, disk, folder };
47
+ }
48
+
49
+ export function makeEngine(title, scripts) {
50
+ // (a ProDOS title's memory starts with what ProDOS leaves, as the original's does)
51
+ const memory = { ...(title.disk?.kind === 'prodos' ? started(title.disk) : {}), ...title.memory };
52
+ const engine = new Engine({ routines: ENGINES[title.engine](title), scripts, start: title.start, label: title.startLabel, memory });
53
+ engine.title = title;
54
+ return engine;
55
+ }
package/remake/main.js ADDED
@@ -0,0 +1,51 @@
1
+ // The remake in the browser: one player for every title. The page gives it the title's folder and its game's data;
2
+ // the player makes its part of the page, the stage the game is drawn on and the options line under it with the
3
+ // game's status, loads the title (load.js), puts its stylesheet in the page where it has one, and hands the game to
4
+ // its engine: the remake's own for a title of scenes, 'card' or 'vector' (scenes.js), or the title's own, a module in
5
+ // its folder (its title.js `engine`), given the same. A failure is said in the status line.
6
+ //
7
+ // await play({ title, data, root, disk, preview, words, watch, claim, pace, route, mini, roms })
8
+ // title the title's folder, an absolute URL
9
+ // data its game's data, an absolute URL (for a title of its own engine, the folder its data is in)
10
+ // root the page's section the player is made in
11
+ // disk the title's player's disk (disk/disk.js), opened before the player starts
12
+ // preview the preview's build: what only it has may be used
13
+ // words the page's words the player shows: { printer: { line, print, tear } }
14
+ // watch called with the game, { engine, press, quick }, for a page that follows it
15
+ // claim k => true to take a key of the player's before the game does
16
+ // pace how much of the original's waits: 1 all (the default), 0 none, between a part of each; read at
17
+ // each wait
18
+ // and what only a workshop has, given by its pages, never looked for here:
19
+ // route a route to play first, { route, keys }: its keys, and the pilots' key handling (expand, give,
20
+ // petscii, pollsAt)
21
+ // mini a mini view for a title with an `inset`, (at, { memory }) => { mode(m) }
22
+ // roms the makers' ROMs this machine has: { chargen } for a Commodore 64 title's characters
23
+ //
24
+ // What the engine is handed, its own engine's or the remake's, `game`: { title, scripts, disk, folder, stage,
25
+ // options, status, fail, kept, preview, words, ...the page's options }; `stage` is its to draw in, `options` the
26
+ // line its own controls go on, before the status.
27
+ import { loadTitle } from './load.js';
28
+ import { play as scenes } from './scenes.js';
29
+
30
+ export async function play(options) {
31
+ const { title: folder, data, root, disk: kept, preview = false, words } = options;
32
+ root.classList.add('remake');
33
+ root.innerHTML = '<div class="stage"></div><p class="options"><span id="status">Loading.</span></p>';
34
+ const stage = root.querySelector('.stage'), line = root.querySelector('.options'), status = root.querySelector('#status');
35
+ const fail = err => { status.textContent = 'The remake stopped: ' + err.message; status.className = 'error'; console.error(err); };
36
+ try {
37
+ // ?fixed: the title with its bugs fixed, for one that has such a version (its title.js `fixed`)
38
+ const fixed = new URLSearchParams(location.search).has('fixed');
39
+ const { title, scripts, disk } = await loadTitle(folder, { data, fixed });
40
+ title.disk = disk;
41
+ // the title's own styles, where it has them (its title.js `style`, a file in its folder), in place before it
42
+ // draws by them
43
+ if (title.style && !document.querySelector(`link[data-title="${title.slug}"]`)) {
44
+ const link = Object.assign(document.createElement('link'), { rel: 'stylesheet', href: new URL(title.style, folder).href });
45
+ link.dataset.title = title.slug; document.head.append(link);
46
+ await new Promise(done => { link.onload = link.onerror = done; });
47
+ }
48
+ const game = { ...options, title, scripts, disk, folder, data, stage, options: line, status, fail, kept, preview, words, fixed };
49
+ await (title.own ?? { play: scenes }).play(game);
50
+ } catch (err) { fail(err); }
51
+ }
@@ -0,0 +1,43 @@
1
+ // A problem, rewritten for the remake: a generator function that takes the problem's helpers
2
+ // and yields whenever it waits for a key. Its steps are the same routines the scripts use, so
3
+ // they give the same events; a problem's own drawing routines are steps of their own, named in
4
+ // its title's map (`problems`), which the view draws however it likes.
5
+ //
6
+ // function* (p) { yield* p.call('text', 'Hello'); const { x } = yield* p.call('ask', 202); }
7
+ import { describe } from './events.js';
8
+
9
+ export function helpers(e, routines) {
10
+ const zero = e.title.map.vars.zero;
11
+ // a step: run its routine, and wait for as long as it waits
12
+ function* call(name, ...args) {
13
+ const token = { words: [name] }; // what the view reads to know what the problem waits for
14
+ const words = args.map(a => typeof a === 'string' && name === 'text' ? { string: a } : typeof a === 'number' ? String(a) : a);
15
+ for (;;) {
16
+ const r = routines[name](e, words, token);
17
+ if (r !== 'wait') return { a: e.a, x: e.x, y: e.y, c: e.c };
18
+ yield 'wait';
19
+ }
20
+ }
21
+ // a problem's own drawing routine: its event, and what it leaves for the view
22
+ // and the title's painter for it, if it has one (titles/<title>/paint.js), paints it into the
23
+ // model of the original's screen
24
+ const drawn = (name, a = 0, x, y) => { e.a = a & 0xff; if (x !== undefined) e.x = x & 0xff; if (y !== undefined) e.y = y & 0xff; e.title.paint?.[name]?.(e.title.model, e); e.emit(describe(name, [], e, undefined, z => e.mem[z + zero], a => e.mem[a])); };
25
+ // A key, for a problem that reads keys itself: it waits, and the view shows what `kind` of wait
26
+ // it is, 'wait' for the space bar's button or 'ask' for the keys e.askMode asks for.
27
+ function* key(kind = 'wait') {
28
+ const token = { words: [kind] };
29
+ for (;;) {
30
+ const k = e.key();
31
+ // control-E: the Main Menu, from the key reader, as at any key the original read
32
+ if (k === 5 && e.title?.menu) e.leave(...e.title.menu);
33
+ // control-E or control-T in Nature Park: its menus, and back to waiting here
34
+ if (e.title?.visit?.[k]) { e.visit(k); e.waitingOn = token; yield 'wait'; continue; }
35
+ if (k !== undefined) { e.waitingOn = null; return k; }
36
+ e.waitingOn = token;
37
+ yield 'wait';
38
+ }
39
+ }
40
+ // steps done inside a routine of the problem's own: drawn, and no steps of the transcript
41
+ function* inside(run) { e.inside++; try { return yield* run; } finally { e.inside--; } }
42
+ return { e, mem: e.mem, call, drawn, key, inside };
43
+ }