@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,363 @@
1
+ // The routines of a vector engine title, for the remake engine, as its title.js `map` names them: what each step
2
+ // of a scene does. Each gives the event the original's routine gives
3
+ // (events.js), does what the original did to the registers, the flags and the memory that the
4
+ // scenes read, and paints the original's screen for the scenes that read it back. The rules for
5
+ // the keys, the random numbers and the waits are the original's, from PART0 (docs in
6
+ // @moonbase-machines/vector's README).
7
+ import { describe, result, TAKES, nothing } from './events.js';
8
+ import { helpers } from './problems.js';
9
+ import { tables, carry } from '@moonbase-machines/vector/objects.js';
10
+ import { pictures } from '@moonbase-machines/vector/pictures.js';
11
+ import { width, useFont } from '@moonbase-machines/vector/font.js';
12
+ import { Raster } from './raster.js';
13
+ import { Files } from '@moonbase-machines/apple2e/mli.js';
14
+ import { layout, resolve, PANELS, textLayout, textPen, textPage } from './layout.js';
15
+
16
+ // A vector title's map (its title.js `map`): `volume`, its disk's volume; `objects`, the routine that sets the table
17
+ // of objects; `seed` ({ at, used }), where the random routine keeps its seed and the byte that is $FF until its first
18
+ // call; `menu`, the Main Menu, which the key reader jumps to on control-E; `visit` ({ at, kept, back }), a way to its
19
+ // menus instead, the screen kept and put back (Nature Park's); `keyPanel`, the arrows' panel; `vars`, where the
20
+ // engine keeps what the remake reads (`zero`, how far up the zero page its routines' inputs are; the screen's
21
+ // layout; the difficulty; practice; repeats; the sound flag, on with its `soundOn` bit); `loads`, where its code
22
+ // files load when its catalog has lost it; `askClearsCarry`, `countInX`; `modern`, characters a modern font draws
23
+ // otherwise; `routines`, its engine's routines by address, each the name of its step; `problems`, each problem's own
24
+ // routines that draw, by the problem's number.
25
+
26
+ // A game's code files, PART0, the parts and the problems, each with the address it loads at: the map's `loads`, by
27
+ // the file's name or its name without the digit, or else the catalog's
28
+ export function codeFiles(map, volume) {
29
+ const loads = map?.loads ?? {};
30
+ return Object.entries(volume).filter(([n]) => /^(PART|PROB)\d$/.test(n))
31
+ .map(([name, f]) => ({ name, bytes: Uint8Array.from(atob(f.data), c => c.charCodeAt(0)), origin: loads[name] ?? loads[name.slice(0, -1)] ?? f.aux }));
32
+ }
33
+
34
+ const KEY = { escape: 27, space: 32, return: 13, delete: 127, left: 8, right: 21, down: 10, up: 11, menu: 5 };
35
+
36
+ // the inputs of a step: numbers, a register's name, or an address for the table of objects
37
+ function inputs(e, name, args) {
38
+ const takes = TAKES[name] || '', fixed = [];
39
+ let k = 0;
40
+ if (name === 'panel') fixed.push(args[k++]);
41
+ const regs = { a: e.a, x: e.x, y: e.y };
42
+ if (name === 'objects' && args[0]?.startsWith?.('$')) { const t = parseInt(args[k++].slice(1), 16); regs.x = t & 0xff; regs.y = t >> 8; }
43
+ else for (const r of takes) {
44
+ const v = args[k++];
45
+ if (v === undefined || v === r) continue;
46
+ regs[r] = typeof v === 'object' ? v.string.charCodeAt(0) | 0x80 : v.startsWith('$') ? e.mem[parseInt(v.slice(1), 16)] : +v;
47
+ }
48
+ e.a = regs.a & 0xff; e.x = regs.x & 0xff; e.y = regs.y & 0xff;
49
+ return { fixed, rest: args.slice(k) };
50
+ }
51
+
52
+ // A step that takes a key: its event once, then it waits until a key comes. Control-E, at any
53
+ // key the original read, went to the Main Menu and dropped everything called.
54
+ function waiting(e, step, name, fixed, words, take) {
55
+ if (e.waitingOn !== step) { e.emit(describe(name, fixed, e, words)); e.waitingOn = step; e.asked = null; }
56
+ for (;;) {
57
+ const k = e.key();
58
+ if (k === undefined) return 'wait';
59
+ if (k === KEY.menu && e.title.menu) e.leave(...e.title.menu);
60
+ if (e.title.visit?.[k]) { e.visit(k); return 'wait'; }
61
+ if (take(k)) { e.waitingOn = null; return; }
62
+ }
63
+ }
64
+
65
+ // $6415 calls the problem's own routines as a choice is made and taken back ($F729 and $F74B,
66
+ // then $F776 and $F6CE, in PROB2): drawing that happens inside the ask. The problem gives them as
67
+ // generators in e.askHooks, which run to their end here, inside, so they are drawn and no step.
68
+ function quietly(e, run) {
69
+ if (!run) return;
70
+ e.inside++;
71
+ try { for (let r = run.next(); !r.done; r = run.next()) if (r.value === 'wait') throw new Error('a choice drawn inside the ask cannot wait'); }
72
+ finally { e.inside--; }
73
+ }
74
+
75
+ // The disk's objects, by the address of their table, for the carry the object routine leaves.
76
+ // `title.disk` is the title's disk from its game's data.
77
+ function objectsOf(title) {
78
+ if (title.objectTables || !title.disk) return title.objectTables;
79
+ const files = codeFiles(title.map, Object.values(title.disk.volumes)[0]);
80
+ return title.objectTables = new Map(tables(files, title.map.objects).map(t => [t.address, t.objects]));
81
+ }
82
+
83
+ // The disk's pictures, by name, for the screen model
84
+ function picturesOf(title) {
85
+ if (title.pictureShapes || !title.disk) return title.pictureShapes;
86
+ const volume = Object.values(title.disk.volumes)[0], map = new Map();
87
+ for (const [name, f] of Object.entries(volume)) if (/^PICS\d$/.test(name)) for (const p of pictures(Uint8Array.from(atob(f.data), c => c.charCodeAt(0)))) map.set(p.name, p.shapes);
88
+ return title.pictureShapes = map;
89
+ }
90
+
91
+ // a picture's shapes by its name, as the disk has them, for a problem that works with them
92
+ export const pictureShapes = (title, name) => picturesOf(title)?.get(name.toUpperCase().replace(/_/g, ' '));
93
+
94
+ export function vectorRoutines(title) {
95
+ // where this engine keeps its variables, and its routines' inputs in the zero page
96
+ const V = title.map.vars, Z = z => V.zero + z;
97
+ useFont(title.font);
98
+ // what an object's routine leaves: the carry, and in the row its rows were counted in (zero
99
+ // page $02 from the engine's), the row past the object's bottom, which a span after it may take
100
+ const objectCarry = e => {
101
+ if (e.a < 2) return; // putting ground back ends another way; no scene is known to use its carry
102
+ const o = objectsOf(title)?.get(e.objectTable)?.find(o => o.number === e.a);
103
+ if (o) { e.c = carry(o, e.x); e.mem[Z(0x02)] = (e.y + o.height) & 0xff; }
104
+ };
105
+ // The original's screen, dot for dot (raster.js), which each step paints as the original's
106
+ // routine painted, for read-dot; and the text state that lasts from text to text.
107
+ const raster = title.raster ??= new Raster();
108
+ const pen = title.pen ??= { x: 0, y: 0, margin: 0, centre: false, spacing: 1, gap: 0, ink: 15, paper: 10 };
109
+ // The text screen, where the menus are: its characters, its pen, and whether it is showing.
110
+ // `show` ($5BA9) puts the graphics back.
111
+ title.text ??= { showing: false, pen: textPen(), page: textPage() };
112
+ const objectOf = (e, n) => objectsOf(title)?.get(e.objectTable)?.find(o => o.number === n);
113
+ const drawObject = (e, how) => { if (e.a < 2) return raster.restore(); const o = objectOf(e, e.a); if (o) raster.object(o, e.x, e.y, how); };
114
+ // a key cap at the pen, as $76F3 draws it: outlined in color 13 and filled with 1, four dots
115
+ // wider than the glyph or than four, and the glyph in 15 from a dot on, after a spacer that
116
+ // brings a narrow glyph to the cap's right; the pen is left two dots past the text's end
117
+ const cap = ch => {
118
+ const w = width(ch), x = pen.x, spacer = (w < 4 ? 4 - w : 0) + 1;
119
+ raster.rectangle(1, x, pen.y - 2, Math.max(4, w) + 4, 11); raster.outline(13, x, pen.y - 2, Math.max(4, w) + 4, 11); // ($5C83)
120
+ raster.glyph(ch, x + 1 + spacer, pen.y, 15, 1, 1, 8); // its text's lines are 8 high
121
+ pen.x = x + 1 + spacer + w + 1 + 2;
122
+ };
123
+ // the arrows' panel, all four caps (n 0) or the nth, as the ask draws it in its arrow mode
124
+ const keyPanel = n => {
125
+ const k = title.map.keyPanel;
126
+ if (!k) return;
127
+ const c = raster.get(2, 2), { x, y } = pen;
128
+ for (let r = k.top; r < k.top + k.rows; r++) raster.span(c, k.left, k.right, r);
129
+ k.caps.forEach(([cx, cy, ch], i) => { if (!n || n === i + 1) { pen.x = cx; pen.y = cy; cap(String.fromCharCode(ch & 0x7f)); } });
130
+ Object.assign(pen, { x, y });
131
+ };
132
+ // a text on the graphics screen, as $6CE2 paints it
133
+ const print = (e, words) => { for (const line of layout(resolve(words, e.mem), pen)) for (const c of line.chars) raster.glyph(c.ch, c.x, line.top, c.ink ?? line.ink, line.paper, line.tall ? 2 : 1, line.height, pen.spacing); };
134
+ // object n of the table set last, put as `how` ('whole', 'keep' or 'clear')
135
+ const put = (e, n, x, y, how) => { const o = objectOf(e, n); if (o) raster.object(o, x, y, how); };
136
+ // what a title's painters (titles/<title>/paint.js) paint the model with
137
+ title.model = { raster, pen, cap, keyPanel, print, put };
138
+ const simple = (name, draw) => (e, args, step) => {
139
+ let { fixed, rest } = inputs(e, name, args);
140
+ const words = rest.map(r => typeof r === 'object' ? r.string : r).join(' ');
141
+ e.emit(describe(name, fixed, e, rest.length ? words : undefined, z => e.mem[Z(z)]));
142
+ rest = rest.map(r => typeof r === 'object' ? r.string : r);
143
+ draw?.(e, fixed, rest);
144
+ };
145
+ // $641B: A $80 asks for an arrow, $B1 to $B9 a digit from 1, any other a letter from A. The
146
+ // key chosen is then confirmed with return, or taken back with delete or the left arrow.
147
+ // Gives X the key's place from 0, Y twice that, and A the key, its high bit set from 'A' up.
148
+ // Animal Rescue's leaves the carry set for a letter ($6572); Nature Park's clears it ($5A56).
149
+ // Nature Park's $58DA, ask-escape, also takes escape, at either key, and gives A 27 and the
150
+ // carry set, and X the keyboard's byte for it, $9B, as the key read left it ($5A58, a jump into
151
+ // the middle of a BIT that leaves only SEC and RTS).
152
+ const letterCarry = !title.map.askClearsCarry;
153
+ const asking = name => (e, args, step) => {
154
+ if (e.waitingOn !== step) inputs(e, name, args);
155
+ const mode = e.waitingOn === step ? e.askMode : e.a;
156
+ e.askMode = mode;
157
+ const arrows = [KEY.left, KEY.right, KEY.down, KEY.up];
158
+ if (mode === 0x80 && e.waitingOn !== step) keyPanel(0);
159
+ const r = waiting(e, step, name, [], undefined, k => {
160
+ if (name === 'ask-escape' && k === KEY.escape) { e.a = e.flags(27); e.x = 0x9b; e.c = 1; e.asked = null; return true; }
161
+ if (e.asked === null) {
162
+ let place = -1, key = k;
163
+ if (mode === 0x80) { place = arrows.indexOf(k); key = 0xdb + place; if (place >= 0) keyPanel(place + 1); }
164
+ else if (mode >= 0xb1 && mode < 0xba) { if (k >= 0x31 && k <= (mode & 0x7f)) place = k - 0x31; }
165
+ else { key = k & 0x5f; if (key >= 0x41 && key <= (mode & 0x5f & 0x7f)) { place = key - 0x41; key |= 0x80; } }
166
+ if (place >= 0) { e.asked = { place, key }; if (name === 'ask-mode') quietly(e, e.askHooks?.chose?.(place)); }
167
+ return false;
168
+ }
169
+ if (k === KEY.return) { e.x = e.asked.place; e.y = 2 * e.x; e.a = e.asked.key; e.c = letterCarry && e.a >= 0xc1 ? 1 : 0; return true; }
170
+ if (k === KEY.delete || k === KEY.left) { if (name === 'ask-mode') quietly(e, e.askHooks?.undo?.(e.asked.place)); if (mode === 0x80) keyPanel(0); e.asked = null; }
171
+ return false;
172
+ });
173
+ if (r !== 'wait') { e.mem[V.layout] = 0x22; e.emit(result(name, e)); } // $655E: panel 6, which sets $22
174
+ return r;
175
+ };
176
+
177
+ const routines = {
178
+ // graphics shown: $C01D reads with its high bit set, as a scene may test before it shows them
179
+ show: simple('show', e => { title.text.showing = false; e.mem[0xc01d] = 0x80; }),
180
+ // $573D: the text screen cleared ($7645) and shown ($5BCB), which the view draws and is no step
181
+ 'text-screen': e => {
182
+ Object.assign(title.text, { showing: true, page: textPage() });
183
+ e.mem[0xc01d] = 0;
184
+ e.inside++; e.emit({ step: 'text-screen' }); e.inside--;
185
+ },
186
+ picture: simple('picture', (e, f, [name]) => { const shapes = picturesOf(title)?.get(name.toUpperCase().replace(/_/g, ' ')); if (shapes) raster.picture(shapes); }),
187
+ box: simple('box', e => raster.box(e.mem[V.layout], e.a, e.x, e.y)),
188
+ // a panel sets the layout it clears to, as the original's panels do ($6299 and the rest)
189
+ panel: simple('panel', (e, [n]) => {
190
+ const p = PANELS[n];
191
+ if (!p) return;
192
+ e.mem[V.layout] = p.layout;
193
+ raster.panel(p.rows[0], p.rows[1], p.fill);
194
+ for (const y of p.more ?? []) raster.whiteLines(y);
195
+ Object.assign(pen, { paper: p.paper, ink: p.ink, centre: true, spacing: 1, gap: p.gap, y: p.y });
196
+ }),
197
+ // A code's byte may be written {3:$63D8}, and a character {$4DA5}, for a byte the scene's code
198
+ // writes into its own text before printing it: it is read from memory when the text is shown.
199
+ // On the text screen the text goes into its characters, and the graphics stay as they are.
200
+ text: (e, args) => {
201
+ const words = args[0].string
202
+ .replace(/\{([0-9a-f]+):([^}]*)\}/g, (all, code, bytes) => '{' + code + ':' + bytes.split(',').map(b => b.startsWith('$') ? e.mem[parseInt(b.slice(1), 16)].toString(16) : b).join(',') + '}')
203
+ .replace(/\{\$([0-9A-Fa-f]{4})\}/g, (all, at) => String.fromCharCode(e.mem[parseInt(at, 16)] & 0x7f));
204
+ e.emit(describe('text', [], e, words));
205
+ if (title.text.showing) { for (const c of textLayout(resolve(words, e.mem), title.text.pen)) if (title.text.page[c.row]?.[c.col]) title.text.page[c.row][c.col] = { ch: c.ch, inverse: c.inverse }; return; }
206
+ print(e, words);
207
+ },
208
+ objects: simple('objects', e => { e.objectTable = e.x | e.y << 8; }),
209
+ // an object drawn leaves the carry the original's routine left ($6B0F), which a scene may add in
210
+ object: simple('object', e => { drawObject(e, 'whole'); objectCarry(e); }),
211
+ 'object-keep': simple('object-keep', e => { drawObject(e, 'keep'); objectCarry(e); }),
212
+ // $6863: the clear color shows the ground, from a buffer of its own, and no ground is kept
213
+ 'object-mode': simple('object-mode', e => drawObject(e, 'clear')),
214
+ // $76ED sets the pen to X and Y, and goes on into $76F3
215
+ 'key-at': simple('key-at', e => { pen.x = e.x; pen.y = e.y; cap(String.fromCharCode(e.a & 0x7f)); }),
216
+ key: simple('key', e => cap(String.fromCharCode(e.a & 0x7f))),
217
+ 'white-lines': simple('white-lines', e => raster.whiteLines(e.y)),
218
+ 'fill-rows': simple('fill-rows', e => raster.rows(e.a, e.x, e.y, e.c)),
219
+ span: simple('span', e => raster.span(e.a, e.x, e.y, e.mem[Z(0x02)])),
220
+ // a dot, which returns with A its place in its group of seven times 16, and its color ($5B45)
221
+ dot: simple('dot', e => { raster.set(e.x, e.y, e.a); e.a = (e.x % 7) << 4 | e.a & 15; }),
222
+ line: simple('line', e => raster.line(e.mem[Z(0x11)], e.mem[Z(0x00)], e.mem[Z(0x02)], e.mem[Z(0x01)], e.mem[Z(0x03)])),
223
+ // $67B9 fills columns x to x + $0E, both ends, down $0F rows; $677E's four lines go from x to
224
+ // x + $0E and from y to y + $0F
225
+ rectangle: simple('rectangle', e => raster.rectangle(e.a, e.x, e.y, e.mem[Z(0x0e)] + 1, e.mem[Z(0x0f)])),
226
+ 'rectangle-outline': simple('rectangle-outline', e => raster.outline(e.a, e.x, e.y, e.mem[Z(0x0e)] + 1, e.mem[Z(0x0f)] + 1)),
227
+ flash: simple('flash'),
228
+ // $5B4C: the color of a dot of the original's screen, from the screen model
229
+ 'read-dot': (e, args) => { inputs(e, 'read-dot', args); e.emit(describe('read-dot', [], e)); e.a = e.flags(raster.get(e.x, e.y)); e.emit(result('read-dot', e)); },
230
+ 'key-panel': simple('key-panel', e => keyPanel(e.a)), 'keep-span': simple('keep-span'), 'put-span': simple('put-span'), 'invert-span': simple('invert-span'),
231
+ button: simple('button'),
232
+ // a beep of 0 does nothing, and is no step (events.js)
233
+ beep: (e, args) => { inputs(e, 'beep', args); if (!nothing('beep', e.a)) e.emit(describe('beep', [], e)); },
234
+ // a sound's five bytes; a byte may be an address, for one the scene's code writes before it plays
235
+ sound: (e, args) => e.emit(describe('sound', [], e, args.map(b => b.startsWith('$') ? e.mem[parseInt(b.slice(1), 16)].toString(16) : b).join(' '))),
236
+ // PART3's own code, written at $E000, moves the picture a step to the left: drawn, and no step
237
+ scroll: e => { raster.scroll(); e.inside++; e.emit({ step: 'scroll' }); e.inside--; },
238
+ // Nature Park's PART3 at $9796: the dots from `left` to `right` of rows `top` to `bottom` copied
239
+ // `by` rows down, from the bottom row up, so the rows at the top stay to be copied again:
240
+ // drawing, and no step
241
+ 'shift-down': (e, args) => {
242
+ const [left, right, top, bottom, by] = args.map(Number);
243
+ for (let r = bottom; r >= top; r--) for (let x = left; x <= right; x++) raster.set(x, r + by, raster.get(x, r));
244
+ e.inside++; e.emit({ step: 'shift-down', left, right, top, bottom, by }); e.inside--;
245
+ },
246
+ // a part's title marks where the original's loader starts a part
247
+ part: simple('part'),
248
+
249
+ // the waits, $5C23, $5C08 and $5C20: the space bar, or for the last escape too, which it
250
+ // returns with the carry set
251
+ wait: (e, args, step) => waiting(e, step, 'wait', [], undefined, k => k === KEY.space && (e.a = 32, e.c = 0, true)),
252
+ 'wait-erase': (e, args, step) => waiting(e, step, 'wait-erase', [], undefined, k => k === KEY.space),
253
+ 'wait-or-escape': (e, args, step) => {
254
+ const r = waiting(e, step, 'wait-or-escape', [], undefined, k => (k === KEY.space || k === KEY.escape) && (e.c = k === KEY.escape ? 1 : 0, e.a = k, true));
255
+ if (r !== 'wait') e.emit(result('wait-or-escape', e));
256
+ return r;
257
+ },
258
+
259
+ // $641B, the ask for a key, above
260
+ ask: asking('ask'),
261
+ // $6415: the same, and the problem draws the choice as it is made, which is left to the view
262
+ 'ask-mode': asking('ask-mode'),
263
+ 'ask-escape': asking('ask-escape'),
264
+
265
+ // $60C2: the seed times 259, divided by 65535 over n. The seed is at $6131 and $6130 says it is
266
+ // in use; a pilot sets it at the first call, as it does on the original.
267
+ random: (e, args) => {
268
+ const { fixed } = inputs(e, 'random', args);
269
+ e.emit(describe('random', fixed, e));
270
+ const n = e.a;
271
+ const { at, used } = title.map.seed;
272
+ if (e.mem[used] === 0xff && title.seed !== undefined) { e.mem[at] = title.seed & 0xff; e.mem[at + 1] = title.seed >> 8; }
273
+ let seed = e.mem[at] | e.mem[at + 1] << 8;
274
+ seed = (seed * 259) & 0xffff;
275
+ e.mem[at] = seed & 0xff; e.mem[at + 1] = seed >> 8; e.mem[used] = n;
276
+ const q = n ? Math.floor(0xffff / n) : 0xffff;
277
+ e.a = Math.floor(seed / q) & 0xff; e.c = 0;
278
+ e.emit(result('random', e));
279
+ },
280
+
281
+ multiply: (e, args) => { inputs(e, 'multiply', args); e.emit(describe('multiply', [], e)); const p = e.a * e.x; e.a = p & 0xff; e.x = p >> 8; e.emit(result('multiply', e)); },
282
+ divide: (e, args) => { inputs(e, 'divide', args); e.emit(describe('divide', [], e)); const d = e.x | e.y << 8, n = e.a || 1; e.a = Math.floor(d / n) & 0xff; e.x = d % n; e.emit(result('divide', e)); },
283
+
284
+ // a problem, written by hand for the remake (its title's problems.js). It runs
285
+ // as its own script or function and returns here; the carry comes back clear, as it does on
286
+ // the original when the problem loaded. Nature Park's ($4DC3) takes the number of times from X
287
+ // for a problem from the Problems menu, in place of the teacher's, until the problem is done.
288
+ problem: (e, args, step) => {
289
+ if (!e.problem) {
290
+ inputs(e, 'problem', args); e.emit(describe('problem', [], e)); e.mem[V.practice] = e.c ? 0x80 : 0;
291
+ e.repeats = e.mem[V.repeats];
292
+ if (title.map.countInX && e.c) e.mem[V.repeats] = e.x;
293
+ const run = title.problems?.[e.a];
294
+ if (!run) throw new Error('problem ' + e.a + ' is not written yet');
295
+ // the problem's file, where the loader puts it, for the tables a problem reads from it
296
+ const file = title.disk && codeFiles(title.map, Object.values(title.disk.volumes)[0]).find(f => f.name === 'PROB' + e.a);
297
+ if (file) e.mem.set(file.bytes, file.origin);
298
+ e.problem = run(helpers(e, routines));
299
+ }
300
+ if (!e.problem.next().done) return 'wait';
301
+ e.problem = null; e.c = 0; e.mem[V.repeats] = e.repeats; e.emit(result('problem', e));
302
+ },
303
+
304
+ // $5D66 and $5DA9: a key, as the title's own loops read it; A the key, the carry set for escape
305
+ 'read-key': (e, args, step) => {
306
+ if (e.waitingOn !== step) { e.waitingOn = step; e.asked = null; }
307
+ const k = e.key();
308
+ if (k === undefined) return 'wait';
309
+ if (title.visit?.[k]) { e.visit(k); return 'wait'; }
310
+ e.waitingOn = null;
311
+ if (k === KEY.menu && title.menu) e.leave(...title.menu);
312
+ e.a = e.flags(k & 0x7f); e.c = k === KEY.escape ? 1 : 0;
313
+ e.emit(result('read-key', e));
314
+ },
315
+
316
+ // The loader's splash, before PART0 runs: TITLE, a double hi-res picture, until a key. The
317
+ // original's engine is not in yet, so it is no step; the view draws it.
318
+ splash: (e, args, step) => {
319
+ if (e.waitingOn !== step) { e.inside++; e.emit({ step: 'splash' }); e.inside--; e.waitingOn = step; }
320
+ if (e.key() === undefined) return 'wait';
321
+ e.waitingOn = null;
322
+ },
323
+ // Nature Park's $4C00: both halves of the graphics screen cleared to black, which is no step
324
+ 'clear-screen': e => {
325
+ for (const row of raster.dots) row.fill(0);
326
+ e.inside++; e.emit({ step: 'clear-screen' }); e.inside--;
327
+ },
328
+
329
+ // A delay the original made by counting X and Y down, given as the milliseconds it took: no
330
+ // step, and nothing changes but the registers, both 0 when the count is done. The page waits.
331
+ pause: (e, [ms]) => { e.x = 0; e.y = 0; e.flags(0); e.inside++; e.emit({ step: 'pause', ms: +ms }); e.inside--; },
332
+
333
+ // End, on the Main Menu: ProDOS's quit, through $0200, and the title is over
334
+ quit: e => { e.ended = true; },
335
+
336
+ // a part of the title not written yet: the run stops here and says which
337
+ unwritten: (e, [what]) => { throw new Error('not written yet: ' + (what?.string ?? what)); },
338
+
339
+ // a jump or a call into PART0's own code, which the title's scripts have: the title's
340
+ // `entries` say where each address is
341
+ 'goto-engine': (e, [at]) => { const to = title.entries?.[at]; if (!to) throw new Error('no script for ' + at); e.enter(...to); },
342
+ 'call-engine': (e, [at]) => { const to = title.entries?.[at]; if (!to) throw new Error('no script for ' + at); e.stack.push({ name: e.name, pc: e.pc }); e.enter(...to); },
343
+
344
+ // the loader: part A's script, from its start, after the title has shown it
345
+ // (and, with the disk out of its drive, it comes back, as the loader's read does with its error)
346
+ 'load-part': (e) => {
347
+ const volume = Object.keys(title.disk?.volumes ?? {})[0]?.toUpperCase();
348
+ if (title.files?.out?.has(volume)) { e.a = e.flags(0x45); e.c = 1; return; }
349
+ e.stack = []; e.enter('part' + e.a, undefined, true);
350
+ },
351
+
352
+ // Nature Park's settings written to its own disk through the loader ($08E9): ProDOS's
353
+ // SET_FILE_INFO, answered from the disks as @moonbase-machines/apple2e/mli.js answers the original's call: the
354
+ // write-protect error ($2B) with the tab on, no volume ($45) with the disk out of its drive,
355
+ // and the carry set for either; clear when it goes through
356
+ 'write-settings': e => {
357
+ const files = title.files ??= new Files(title.disk), volume = Object.keys(title.disk.volumes)[0].toUpperCase();
358
+ const err = files.out?.has(volume) ? 0x45 : files.disks[volume] === 'protected' ? 0x2b : 0;
359
+ e.a = e.flags(err); e.c = err ? 1 : 0;
360
+ },
361
+ };
362
+ return routines;
363
+ }