jsbeeb 1.11.0 → 1.13.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/README.md +17 -3
- package/package.json +8 -9
- package/public/roms/atom/ATMMC3E.rom +0 -0
- package/public/roms/atom/Atom_Basic.rom +0 -0
- package/public/roms/atom/Atom_DOS.rom +0 -0
- package/public/roms/atom/Atom_FloatingPoint.rom +0 -0
- package/public/roms/atom/Atom_Kernel.rom +0 -0
- package/public/roms/atom/Atom_Kernel_E.rom +0 -0
- package/public/roms/atom/PCHARME.ROM +0 -0
- package/public/roms/atom/gags.rom +0 -0
- package/public/roms/atom/werom.rom +0 -0
- package/src/6502.js +351 -44
- package/src/6847.js +724 -0
- package/src/6847_fontdata.js +124 -0
- package/src/app/app.js +1 -1
- package/src/app/electron.js +4 -4
- package/src/bem-snapshot.js +5 -184
- package/src/config.js +20 -9
- package/src/disc.js +2 -20
- package/src/fake6502.js +3 -2
- package/src/jsbeeb.css +23 -0
- package/src/keyboard.js +62 -37
- package/src/machine-session.js +85 -59
- package/src/main.js +188 -75
- package/src/mmc.js +1053 -0
- package/src/models.js +46 -5
- package/src/ppia.js +477 -0
- package/src/snapshot-helpers.js +208 -0
- package/src/snapshot.js +9 -18
- package/src/soundchip.js +99 -1
- package/src/tapes.js +73 -16
- package/src/uef-snapshot.js +402 -0
- package/src/url-params.js +7 -2
- package/src/utils.js +81 -2
- package/src/utils_atom.js +508 -0
- package/src/video.js +12 -1
- package/src/web/audio-handler.js +39 -17
- package/tests/test-machine.js +133 -8
package/src/keyboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
import * as utils from "./utils.js";
|
|
3
|
-
import
|
|
3
|
+
import { ATOM } from "./utils_atom.js";
|
|
4
4
|
|
|
5
5
|
const isMac = typeof window !== "undefined" && /^Mac/i.test(window.navigator?.platform || "");
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ const isMac = typeof window !== "undefined" && /^Mac/i.test(window.navigator?.pl
|
|
|
15
15
|
/**
|
|
16
16
|
* Keyboard class that handles all keyboard related functionality
|
|
17
17
|
*/
|
|
18
|
-
export class Keyboard extends
|
|
18
|
+
export class Keyboard extends EventTarget {
|
|
19
19
|
/**
|
|
20
20
|
* Create a new Keyboard instance with specified configuration
|
|
21
21
|
* @param {KeyboardConfig} config - The configuration object
|
|
@@ -29,6 +29,14 @@ export class Keyboard extends EventEmitter {
|
|
|
29
29
|
this.inputEnabledFunction = inputEnabledFunction;
|
|
30
30
|
this.dbgr = dbgr;
|
|
31
31
|
|
|
32
|
+
// Key interface: routes key events to SysVia (BBC) or PPIA (Atom).
|
|
33
|
+
// Both provide keyDown, keyUp, keyToggleRaw, setKeyLayout,
|
|
34
|
+
// clearKeys, disableKeyboard, enableKeyboard.
|
|
35
|
+
this.keyInterface = processor.model.isAtom ? processor.atomppia : processor.sysvia;
|
|
36
|
+
// The SHIFT key constant used by stringToMachineKeys in the paste key array.
|
|
37
|
+
// Compared by reference in _deliverPasteKey to avoid toggling shift off.
|
|
38
|
+
this._shiftKey = processor.model.isAtom ? ATOM.SHIFT : utils.BBC.SHIFT;
|
|
39
|
+
|
|
32
40
|
// State
|
|
33
41
|
this.emuKeyHandlers = {};
|
|
34
42
|
this.running = false;
|
|
@@ -134,7 +142,7 @@ export class Keyboard extends EventEmitter {
|
|
|
134
142
|
*/
|
|
135
143
|
setKeyLayout(layout) {
|
|
136
144
|
this.keyLayout = layout;
|
|
137
|
-
this.
|
|
145
|
+
this.keyInterface.setKeyLayout(layout);
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
/**
|
|
@@ -182,7 +190,7 @@ export class Keyboard extends EventEmitter {
|
|
|
182
190
|
// Handle debugger 'g' key press
|
|
183
191
|
if (this.dbgr.enabled() && code === LOWERCASE_G) {
|
|
184
192
|
this.dbgr.hide();
|
|
185
|
-
this.
|
|
193
|
+
this.dispatchEvent(new Event("resume"));
|
|
186
194
|
return;
|
|
187
195
|
}
|
|
188
196
|
|
|
@@ -193,7 +201,7 @@ export class Keyboard extends EventEmitter {
|
|
|
193
201
|
return;
|
|
194
202
|
} else if (code === LOWERCASE_N) {
|
|
195
203
|
this.requestStep();
|
|
196
|
-
this.
|
|
204
|
+
this.dispatchEvent(new Event("resume"));
|
|
197
205
|
return;
|
|
198
206
|
}
|
|
199
207
|
}
|
|
@@ -226,7 +234,7 @@ export class Keyboard extends EventEmitter {
|
|
|
226
234
|
const isSpecialHandled = this._handleSpecialKeys(code);
|
|
227
235
|
if (isSpecialHandled) return;
|
|
228
236
|
|
|
229
|
-
// Check for registered handlers first; if one fires, don't
|
|
237
|
+
// Check for registered handlers first; if one fires, don't pass to the emulator.
|
|
230
238
|
// This lets Alt+key and Ctrl+key handlers cleanly own their keys without the
|
|
231
239
|
// underlying key leaking through to the emulated machine.
|
|
232
240
|
const handler = this._findKeyHandler(code, evt.altKey, evt.ctrlKey);
|
|
@@ -235,8 +243,8 @@ export class Keyboard extends EventEmitter {
|
|
|
235
243
|
return;
|
|
236
244
|
}
|
|
237
245
|
|
|
238
|
-
// No handler claimed the key
|
|
239
|
-
this.
|
|
246
|
+
// No handler claimed the key; pass it to the emulated machine.
|
|
247
|
+
this.keyInterface.keyDown(code, evt.shiftKey);
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
/**
|
|
@@ -247,7 +255,7 @@ export class Keyboard extends EventEmitter {
|
|
|
247
255
|
*/
|
|
248
256
|
_handleSpecialKeys(code) {
|
|
249
257
|
if (code === utils.keyCodes.F12 || code === utils.keyCodes.BREAK) {
|
|
250
|
-
this.
|
|
258
|
+
this.dispatchEvent(new CustomEvent("break", { detail: true }));
|
|
251
259
|
this.processor.setReset(true);
|
|
252
260
|
return true;
|
|
253
261
|
} else if (isMac && code === utils.keyCodes.CAPSLOCK) {
|
|
@@ -269,7 +277,7 @@ export class Keyboard extends EventEmitter {
|
|
|
269
277
|
|
|
270
278
|
// Always let the key ups come through to avoid sticky keys in the debugger
|
|
271
279
|
const code = this.keyCode(evt);
|
|
272
|
-
this.
|
|
280
|
+
this.keyInterface.keyUp(code);
|
|
273
281
|
|
|
274
282
|
// No further special handling needed if not running
|
|
275
283
|
if (!this.running) return;
|
|
@@ -278,7 +286,7 @@ export class Keyboard extends EventEmitter {
|
|
|
278
286
|
|
|
279
287
|
// Handle special key cases
|
|
280
288
|
if (code === utils.keyCodes.F12 || code === utils.keyCodes.BREAK) {
|
|
281
|
-
this.
|
|
289
|
+
this.dispatchEvent(new CustomEvent("break", { detail: false }));
|
|
282
290
|
this.processor.setReset(false);
|
|
283
291
|
return;
|
|
284
292
|
} else if (isMac && code === utils.keyCodes.CAPSLOCK) {
|
|
@@ -302,39 +310,44 @@ export class Keyboard extends EventEmitter {
|
|
|
302
310
|
|
|
303
311
|
// Mac browsers seem to model caps lock as a physical key that's down when capslock is on, and up when it's off.
|
|
304
312
|
// No event is generated when it is physically released on the keyboard. So, we simulate a "tap" here.
|
|
305
|
-
this.
|
|
313
|
+
this.keyInterface.keyDown(utils.keyCodes.CAPSLOCK);
|
|
306
314
|
|
|
307
315
|
// Simulate a key release after a short delay
|
|
308
|
-
setTimeout(() => this.
|
|
316
|
+
setTimeout(() => this.keyInterface.keyUp(utils.keyCodes.CAPSLOCK), CAPS_LOCK_DELAY);
|
|
309
317
|
|
|
310
318
|
if (isMac && window.localStorage && !window.localStorage.getItem("warnedAboutRubbishMacs")) {
|
|
311
|
-
this.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
319
|
+
this.dispatchEvent(
|
|
320
|
+
new CustomEvent("showError", {
|
|
321
|
+
detail: {
|
|
322
|
+
context: "handling caps lock on Mac OS X",
|
|
323
|
+
error: `Mac OS X does not generate key up events for caps lock presses.
|
|
324
|
+
jsbeeb can only simulate a 'tap' of the caps lock key. This means it doesn't work well for games
|
|
325
|
+
that use caps lock for left or fire, as we can't tell if it's being held down. If you need to play
|
|
316
326
|
such a game, please see the documentation about remapping keys.
|
|
317
327
|
Close this window to continue (you won't see this error again)`,
|
|
318
|
-
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
319
331
|
window.localStorage.setItem("warnedAboutRubbishMacs", "true");
|
|
320
332
|
}
|
|
321
333
|
}
|
|
322
334
|
|
|
323
335
|
/**
|
|
324
|
-
* Send raw keyboard input to the
|
|
325
|
-
* @param {Array} keysToSend - Array of
|
|
336
|
+
* Send raw keyboard input to the emulated machine (for paste/autotype).
|
|
337
|
+
* @param {Array} keysToSend - Array of machine-specific key codes to send
|
|
326
338
|
* @param {boolean} checkCapsAndShiftLocks - Whether to check caps and shift locks
|
|
327
339
|
*/
|
|
328
|
-
|
|
340
|
+
sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
|
|
329
341
|
if (this.isPasting) this.cancelPaste();
|
|
330
342
|
|
|
331
|
-
this.
|
|
332
|
-
this._pasteClocksPerMs =
|
|
343
|
+
this.keyInterface.disableKeyboard();
|
|
344
|
+
this._pasteClocksPerMs =
|
|
345
|
+
Math.floor(this.processor.cpuMultiplier * this.processor.peripheralCyclesPerSecond) / 1000;
|
|
333
346
|
|
|
334
347
|
if (checkCapsAndShiftLocks) {
|
|
335
348
|
let toggleKey = null;
|
|
336
|
-
if (!this.
|
|
337
|
-
else if (this.
|
|
349
|
+
if (!this.keyInterface.capsLockLight) toggleKey = utils.BBC.CAPSLOCK;
|
|
350
|
+
else if (this.keyInterface.shiftLockLight) toggleKey = utils.BBC.SHIFTLOCK;
|
|
338
351
|
if (toggleKey) {
|
|
339
352
|
keysToSend.unshift(toggleKey);
|
|
340
353
|
keysToSend.push(toggleKey);
|
|
@@ -353,13 +366,22 @@ export class Keyboard extends EventEmitter {
|
|
|
353
366
|
* @private
|
|
354
367
|
*/
|
|
355
368
|
_deliverPasteKey() {
|
|
356
|
-
if (this._pasteLastChar && this._pasteLastChar !==
|
|
357
|
-
this.
|
|
369
|
+
if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey) {
|
|
370
|
+
this.keyInterface.keyToggleRaw(this._pasteLastChar);
|
|
358
371
|
}
|
|
359
372
|
|
|
360
373
|
if (this._pasteKeys.length === 0) {
|
|
361
374
|
this._pasteLastChar = undefined;
|
|
362
|
-
this.
|
|
375
|
+
this.keyInterface.enableKeyboard();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Atom's PPIA keyboard is polled, not interrupt-driven like the BBC's
|
|
380
|
+
// SysVIA. Insert a debounce gap after every key release so the ROM
|
|
381
|
+
// sees the key-up before the next key-down arrives.
|
|
382
|
+
if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey && this.processor.model.isAtom) {
|
|
383
|
+
this._pasteLastChar = undefined;
|
|
384
|
+
this._pasteTask.schedule(30 * this._pasteClocksPerMs);
|
|
363
385
|
return;
|
|
364
386
|
}
|
|
365
387
|
|
|
@@ -372,12 +394,15 @@ export class Keyboard extends EventEmitter {
|
|
|
372
394
|
return;
|
|
373
395
|
}
|
|
374
396
|
|
|
375
|
-
|
|
397
|
+
// Atom ROM polls the keyboard once per VSync (~16ms at 60 Hz).
|
|
398
|
+
// 80ms gives the ROM ~5 scan cycles to detect, debounce, and
|
|
399
|
+
// process each keypress.
|
|
400
|
+
let delayMs = this.processor.model.isAtom ? 80 : 50;
|
|
376
401
|
if (typeof this._pasteLastChar === "number") {
|
|
377
402
|
delayMs = this._pasteLastChar;
|
|
378
403
|
this._pasteLastChar = undefined;
|
|
379
404
|
} else {
|
|
380
|
-
this.
|
|
405
|
+
this.keyInterface.keyToggleRaw(this._pasteLastChar);
|
|
381
406
|
}
|
|
382
407
|
|
|
383
408
|
this._pasteKeys.shift();
|
|
@@ -390,12 +415,12 @@ export class Keyboard extends EventEmitter {
|
|
|
390
415
|
cancelPaste() {
|
|
391
416
|
if (!this.isPasting) return;
|
|
392
417
|
this._pasteTask.cancel();
|
|
393
|
-
if (this._pasteLastChar && this._pasteLastChar !==
|
|
394
|
-
this.
|
|
418
|
+
if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey) {
|
|
419
|
+
this.keyInterface.keyToggleRaw(this._pasteLastChar);
|
|
395
420
|
}
|
|
396
421
|
this._pasteLastChar = undefined;
|
|
397
422
|
this._pasteKeys = [];
|
|
398
|
-
this.
|
|
423
|
+
this.keyInterface.enableKeyboard();
|
|
399
424
|
}
|
|
400
425
|
|
|
401
426
|
/**
|
|
@@ -410,7 +435,7 @@ export class Keyboard extends EventEmitter {
|
|
|
410
435
|
* Clears all pressed keys
|
|
411
436
|
*/
|
|
412
437
|
clearKeys() {
|
|
413
|
-
this.
|
|
438
|
+
this.keyInterface.clearKeys();
|
|
414
439
|
}
|
|
415
440
|
|
|
416
441
|
/**
|
|
@@ -437,7 +462,7 @@ export class Keyboard extends EventEmitter {
|
|
|
437
462
|
*/
|
|
438
463
|
pauseEmulation() {
|
|
439
464
|
this.pauseEmu = true;
|
|
440
|
-
this.
|
|
465
|
+
this.dispatchEvent(new Event("pause"));
|
|
441
466
|
}
|
|
442
467
|
|
|
443
468
|
/**
|
|
@@ -445,6 +470,6 @@ export class Keyboard extends EventEmitter {
|
|
|
445
470
|
*/
|
|
446
471
|
resumeEmulation() {
|
|
447
472
|
this.pauseEmu = false;
|
|
448
|
-
this.
|
|
473
|
+
this.dispatchEvent(new Event("resume"));
|
|
449
474
|
}
|
|
450
475
|
}
|
package/src/machine-session.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readFileSync } from "fs";
|
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { TestMachine } from "../tests/test-machine.js";
|
|
12
|
-
import { InstrumentedSoundChip } from "./soundchip.js";
|
|
12
|
+
import { InstrumentedSoundChip, FakeSoundChip } from "./soundchip.js";
|
|
13
13
|
|
|
14
14
|
// Resolve the jsbeeb package root from our own location (src/machine-session.js
|
|
15
15
|
// → go up one level). Passed to setNodeBasePath() so the ROM loader resolves
|
|
@@ -46,22 +46,31 @@ export class MachineSession {
|
|
|
46
46
|
|
|
47
47
|
// Create a real Video instance so we get pixel output
|
|
48
48
|
const modelObj = findModel(modelName);
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
this._isAtom = modelObj.isAtom;
|
|
50
|
+
this._video = new Video(
|
|
51
|
+
modelObj.isMaster,
|
|
52
|
+
this._fb32,
|
|
53
|
+
(minx, miny, maxx, maxy) => {
|
|
54
|
+
this._lastPaint = { minx, miny, maxx, maxy };
|
|
55
|
+
this._frameDirty = true;
|
|
56
|
+
// Snapshot the complete frame now, before clearPaintBuffer() wipes _fb32.
|
|
57
|
+
// This mirrors what the browser does: paint_ext fires → canvas updated → fb32 cleared.
|
|
58
|
+
this._completeFb8.set(this._fb8);
|
|
59
|
+
},
|
|
60
|
+
{ isAtom: modelObj.isAtom },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Use a real (instrumented) sound chip so we can read registers and capture writes.
|
|
64
|
+
// Atom models use AtomSoundChip which has a different interface (speakerGenerator,
|
|
65
|
+
// toneGenerator); FakeSoundChip provides compatible no-op stubs for headless mode.
|
|
66
|
+
this._soundChip = modelObj.isAtom ? new FakeSoundChip() : new InstrumentedSoundChip();
|
|
59
67
|
|
|
60
68
|
// TestMachine forwards opts.video and opts.soundChip to fake6502
|
|
61
69
|
this._machine = new TestMachine(modelName, { video: this._video, soundChip: this._soundChip });
|
|
62
70
|
|
|
63
71
|
// Accumulated VDU text output — drained by callers
|
|
64
72
|
this._pendingOutput = [];
|
|
73
|
+
this._flushCapture = () => {};
|
|
65
74
|
|
|
66
75
|
// Breakpoint management — persistent hooks that survive across run calls
|
|
67
76
|
this._breakpoints = new Map(); // id → { hook, type, address, hit }
|
|
@@ -90,8 +99,9 @@ export class MachineSession {
|
|
|
90
99
|
/**
|
|
91
100
|
* Install the VDU character-output capture hook.
|
|
92
101
|
*
|
|
93
|
-
* WRCHV discovery: RAM at
|
|
94
|
-
*
|
|
102
|
+
* WRCHV discovery: RAM at the OS write-character vector (0x20E on BBC,
|
|
103
|
+
* 0x208 on Atom) initialises to 0x0000 before the OS runs. We read
|
|
104
|
+
* directly from cpu.ramRomOs (two array lookups — no
|
|
95
105
|
* readmem() dispatch overhead) on every instruction, waiting for the
|
|
96
106
|
* value to change from its initial 0xFFFF. Once the OS installs a real
|
|
97
107
|
* handler we use that address for the lifetime of the session. Programs
|
|
@@ -105,7 +115,9 @@ export class MachineSession {
|
|
|
105
115
|
_installCaptureHook() {
|
|
106
116
|
const cpu = this._machine.processor;
|
|
107
117
|
const ram = cpu.ramRomOs; // direct Uint8Array — no dispatch overhead
|
|
108
|
-
|
|
118
|
+
// WRCHV vector: BBC at $020E, Atom at $0208.
|
|
119
|
+
const wrchvAddr = this._isAtom ? 0x208 : 0x20e;
|
|
120
|
+
const initialWrchv = ram[wrchvAddr] | (ram[wrchvAddr + 1] << 8); // 0xFFFF pre-boot
|
|
109
121
|
|
|
110
122
|
const attributes = { x: 0, y: 0, text: "", foreground: 7, background: 0, mode: 7 };
|
|
111
123
|
let currentText = "";
|
|
@@ -124,6 +136,12 @@ export class MachineSession {
|
|
|
124
136
|
currentText = "";
|
|
125
137
|
}
|
|
126
138
|
|
|
139
|
+
// Expose flush so drainOutput can capture trailing text
|
|
140
|
+
// that hasn't been terminated by a control character.
|
|
141
|
+
this._flushCapture = flush;
|
|
142
|
+
|
|
143
|
+
const isAtom = this._isAtom;
|
|
144
|
+
|
|
127
145
|
function onChar(c) {
|
|
128
146
|
if (nextN) {
|
|
129
147
|
params.push(c);
|
|
@@ -135,9 +153,6 @@ export class MachineSession {
|
|
|
135
153
|
return;
|
|
136
154
|
}
|
|
137
155
|
switch (c) {
|
|
138
|
-
case 1: // next char to printer only
|
|
139
|
-
nextN = 1;
|
|
140
|
-
break;
|
|
141
156
|
case 10: // LF
|
|
142
157
|
flush();
|
|
143
158
|
attributes.y++;
|
|
@@ -151,48 +166,58 @@ export class MachineSession {
|
|
|
151
166
|
flush();
|
|
152
167
|
attributes.x = 0;
|
|
153
168
|
break;
|
|
154
|
-
case 17: // COLOUR n
|
|
155
|
-
nextN = 1;
|
|
156
|
-
vduProc = (p) => {
|
|
157
|
-
if (p[0] & 0x80) attributes.background = p[0] & 0xf;
|
|
158
|
-
else attributes.foreground = p[0] & 0xf;
|
|
159
|
-
};
|
|
160
|
-
break;
|
|
161
|
-
case 18: // GCOL
|
|
162
|
-
nextN = 2;
|
|
163
|
-
break;
|
|
164
|
-
case 19: // define logical colour
|
|
165
|
-
nextN = 5;
|
|
166
|
-
break;
|
|
167
|
-
case 22: // MODE n
|
|
168
|
-
nextN = 1;
|
|
169
|
-
vduProc = (p) => {
|
|
170
|
-
flush();
|
|
171
|
-
attributes.mode = p[0];
|
|
172
|
-
attributes.x = 0;
|
|
173
|
-
attributes.y = 0;
|
|
174
|
-
attributes.foreground = 7;
|
|
175
|
-
attributes.background = 0;
|
|
176
|
-
};
|
|
177
|
-
break;
|
|
178
|
-
case 25: // PLOT
|
|
179
|
-
nextN = 5;
|
|
180
|
-
break;
|
|
181
|
-
case 28: // define text window
|
|
182
|
-
nextN = 4;
|
|
183
|
-
break;
|
|
184
|
-
case 29: // define graphics origin
|
|
185
|
-
nextN = 4;
|
|
186
|
-
break;
|
|
187
|
-
case 31: // TAB(x,y)
|
|
188
|
-
nextN = 2;
|
|
189
|
-
vduProc = (p) => {
|
|
190
|
-
flush();
|
|
191
|
-
attributes.x = p[0];
|
|
192
|
-
attributes.y = p[1];
|
|
193
|
-
};
|
|
194
|
-
break;
|
|
195
169
|
default:
|
|
170
|
+
// BBC VDU control codes (multi-byte sequences).
|
|
171
|
+
// The Atom doesn't use these; skip them to avoid
|
|
172
|
+
// swallowing printable characters.
|
|
173
|
+
if (!isAtom) {
|
|
174
|
+
switch (c) {
|
|
175
|
+
case 1: // next char to printer only
|
|
176
|
+
nextN = 1;
|
|
177
|
+
return;
|
|
178
|
+
case 17: // COLOUR n
|
|
179
|
+
nextN = 1;
|
|
180
|
+
vduProc = (p) => {
|
|
181
|
+
if (p[0] & 0x80) attributes.background = p[0] & 0xf;
|
|
182
|
+
else attributes.foreground = p[0] & 0xf;
|
|
183
|
+
};
|
|
184
|
+
return;
|
|
185
|
+
case 18: // GCOL
|
|
186
|
+
nextN = 2;
|
|
187
|
+
return;
|
|
188
|
+
case 19: // define logical colour
|
|
189
|
+
nextN = 5;
|
|
190
|
+
return;
|
|
191
|
+
case 22: // MODE n
|
|
192
|
+
nextN = 1;
|
|
193
|
+
vduProc = (p) => {
|
|
194
|
+
flush();
|
|
195
|
+
attributes.mode = p[0];
|
|
196
|
+
attributes.x = 0;
|
|
197
|
+
attributes.y = 0;
|
|
198
|
+
attributes.foreground = 7;
|
|
199
|
+
attributes.background = 0;
|
|
200
|
+
};
|
|
201
|
+
return;
|
|
202
|
+
case 25: // PLOT
|
|
203
|
+
nextN = 5;
|
|
204
|
+
return;
|
|
205
|
+
case 28: // define text window
|
|
206
|
+
nextN = 4;
|
|
207
|
+
return;
|
|
208
|
+
case 29: // define graphics origin
|
|
209
|
+
nextN = 4;
|
|
210
|
+
return;
|
|
211
|
+
case 31: // TAB(x,y)
|
|
212
|
+
nextN = 2;
|
|
213
|
+
vduProc = (p) => {
|
|
214
|
+
flush();
|
|
215
|
+
attributes.x = p[0];
|
|
216
|
+
attributes.y = p[1];
|
|
217
|
+
};
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
196
221
|
if (c >= 32 && c < 0x7f) {
|
|
197
222
|
currentText += String.fromCharCode(c);
|
|
198
223
|
} else {
|
|
@@ -207,7 +232,7 @@ export class MachineSession {
|
|
|
207
232
|
// Once the OS sets WRCHV (it changes from 0xFFFF), we start
|
|
208
233
|
// capturing. Programs that install a custom VDU driver mid-run
|
|
209
234
|
// are handled transparently because we re-read on every call.
|
|
210
|
-
const wrchv = ram[
|
|
235
|
+
const wrchv = ram[wrchvAddr] | (ram[wrchvAddr + 1] << 8);
|
|
211
236
|
if (wrchv !== initialWrchv && addr === wrchv) {
|
|
212
237
|
onChar(cpu.a);
|
|
213
238
|
}
|
|
@@ -391,6 +416,7 @@ export class MachineSession {
|
|
|
391
416
|
* Also includes a flat `screenText` reconstruction.
|
|
392
417
|
*/
|
|
393
418
|
drainOutput({ clear = true } = {}) {
|
|
419
|
+
this._flushCapture();
|
|
394
420
|
const elements = clear ? this._pendingOutput.splice(0) : [...this._pendingOutput];
|
|
395
421
|
return {
|
|
396
422
|
elements,
|