jsbeeb 1.22.4 → 1.24.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.
Files changed (112) hide show
  1. package/README.md +2 -2
  2. package/package.json +16 -11
  3. package/src/6502.js +39 -42
  4. package/src/6502.opcodes.js +198 -123
  5. package/src/6847.js +4 -5
  6. package/src/6847_fontdata.js +0 -2
  7. package/src/acia.js +2 -3
  8. package/src/app/app.js +0 -1
  9. package/src/app/args.js +0 -2
  10. package/src/app/electron.js +4 -6
  11. package/src/app-bench.js +1 -3
  12. package/src/archive.js +243 -0
  13. package/src/basic-loader.js +0 -7
  14. package/src/basic-tokenise.js +19 -6
  15. package/src/bbc-palette.js +0 -2
  16. package/src/bbcdiscs.js +0 -2
  17. package/src/bem-snapshot.js +1 -3
  18. package/src/binary.js +175 -0
  19. package/src/cmos.js +0 -2
  20. package/src/ddnoise.js +0 -1
  21. package/src/debounce.js +7 -0
  22. package/src/disc-surface.js +0 -2
  23. package/src/disc.js +10 -9
  24. package/src/fake6502.js +7 -23
  25. package/src/fdc.js +9 -51
  26. package/src/filestore.js +10 -17
  27. package/src/hex.js +38 -0
  28. package/src/index.js +11 -0
  29. package/src/intel-fdc.js +18 -20
  30. package/src/jsbeeb.css +33 -0
  31. package/src/{utils_atom.js → keymap-atom.js} +2 -18
  32. package/src/{utils.js → keymap.js} +47 -610
  33. package/src/loader.js +63 -0
  34. package/src/machine-session.js +37 -177
  35. package/src/machine-spec.js +59 -0
  36. package/src/main.js +69 -184
  37. package/src/media-resolver.js +68 -0
  38. package/src/mmc.js +3 -2
  39. package/src/models.js +40 -4
  40. package/src/music5000.js +0 -2
  41. package/src/ppia.js +3 -4
  42. package/src/printer.js +7 -14
  43. package/src/relaynoise.js +0 -1
  44. package/src/rewind.js +0 -2
  45. package/src/sample-player.js +2 -3
  46. package/src/serial.js +0 -2
  47. package/src/snapshot-helpers.js +0 -2
  48. package/src/snapshot.js +0 -2
  49. package/src/soundchip.js +1 -1
  50. package/src/state-utils.js +0 -2
  51. package/src/sth.js +2 -4
  52. package/src/tapes.js +10 -4
  53. package/src/teletext.js +2 -3
  54. package/src/teletext_adaptor.js +2 -4
  55. package/src/teletext_data.js +0 -2
  56. package/src/test-machine.js +307 -0
  57. package/src/touchscreen.js +2 -4
  58. package/src/tube.js +5 -6
  59. package/src/typist.js +103 -0
  60. package/src/uef-snapshot.js +0 -2
  61. package/src/url-params.js +4 -5
  62. package/src/vdu-capture.js +113 -0
  63. package/src/via.js +3 -4
  64. package/src/video-filters/pal-composite.js +0 -2
  65. package/src/video-filters/passthrough-filter.js +0 -2
  66. package/src/video-filters/pixel-grid.js +0 -2
  67. package/src/video-filters/shader-program.js +0 -2
  68. package/src/video-filters/xbr-filter.js +0 -2
  69. package/src/video.js +12 -11
  70. package/src/wd-fdc.js +20 -20
  71. package/src/web/analogue-inputs.js +6 -8
  72. package/src/web/analytics.js +13 -0
  73. package/src/web/audio-handler.js +2 -2
  74. package/src/{audio-utils.js → web/audio-utils.js} +0 -2
  75. package/src/web/autoboot.js +10 -13
  76. package/src/{canvas.js → web/canvas.js} +4 -5
  77. package/src/web/config.js +42 -72
  78. package/src/web/console-surface.js +25 -0
  79. package/src/web/debug.js +8 -49
  80. package/src/web/disc-visualiser.js +0 -2
  81. package/src/web/display.js +21 -4
  82. package/src/{dom-utils.js → web/dom-utils.js} +1 -11
  83. package/src/web/drives.js +1 -1
  84. package/src/web/emulation-loop.js +54 -33
  85. package/src/web/front-panel.js +13 -12
  86. package/src/{gamepads.js → web/gamepads.js} +2 -6
  87. package/src/web/google-drive-picker.js +14 -9
  88. package/src/{google-drive.js → web/google-drive.js} +9 -10
  89. package/src/web/hfe-picker.js +5 -4
  90. package/src/web/icons.js +42 -0
  91. package/src/web/keyboard-setup.js +29 -40
  92. package/src/{keyboard.js → web/keyboard.js} +18 -119
  93. package/src/web/layout.js +4 -0
  94. package/src/web/local-disc.js +44 -0
  95. package/src/web/machine.js +27 -20
  96. package/src/web/media-loader.js +38 -147
  97. package/src/{microphone-input.js → web/microphone-input.js} +1 -1
  98. package/src/web/modals.js +6 -9
  99. package/src/web/page-actions.js +88 -0
  100. package/src/web/quick-settings.js +16 -15
  101. package/src/web/reporting.js +0 -7
  102. package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
  103. package/src/web/rewind-ui.js +29 -29
  104. package/src/web/run-controls.js +39 -0
  105. package/src/web/settings.js +64 -135
  106. package/src/web/snapshot-ui.js +10 -16
  107. package/src/{speech-output.js → web/speech-output.js} +0 -2
  108. package/src/web/sth-picker.js +9 -7
  109. package/src/web/url-state.js +20 -6
  110. package/src/econet/L3FS.dat +0 -0
  111. package/src/econet/scsi.dat +0 -0
  112. package/tests/test-machine.js +0 -609
@@ -1,609 +0,0 @@
1
- import { basicIdleAddr, installBasic } from "../src/basic-loader.js";
2
- import * as fdc from "../src/fdc.js";
3
- import { fake6502 } from "../src/fake6502.js";
4
- import { findModel } from "../src/models.js";
5
- import assert from "assert";
6
- import * as utils from "../src/utils.js";
7
- import * as utils_atom from "../src/utils_atom.js";
8
- import * as Tokeniser from "../src/basic-tokenise.js";
9
-
10
- const MaxCyclesPerIter = 100 * 1000;
11
-
12
- export class TestMachine {
13
- constructor(model, opts) {
14
- model = model || "B-DFS1.2";
15
- this.model = findModel(model);
16
- if (!this.model) throw new Error(`Unknown model "${model}"`);
17
- this.processor = fake6502(this.model, opts || {});
18
- this._capturedChars = [];
19
- this._captureHookInstalled = false;
20
- }
21
-
22
- /** The keyboard interface for this machine (SysVia for BBC, PPIA for Atom). */
23
- get _keyInterface() {
24
- return this.model.isAtom ? this.processor.atomppia : this.processor.sysvia;
25
- }
26
-
27
- async initialise() {
28
- await this.processor.initialise();
29
- if (this.model.isAtom) this._startAtomVSync();
30
- }
31
-
32
- /**
33
- * The Atom ROM's main loop waits for VSync (bit 7 of Port C) to
34
- * toggle before scanning the keyboard. The MC6847 video chip drives
35
- * this in the real emulator, but fake6502 doesn't create one, so we
36
- * simulate it with a scheduler task at ~60 Hz (NTSC).
37
- */
38
- _startAtomVSync() {
39
- const ppia = this.processor.atomppia;
40
- const VsyncPeriod = 16667; // 1 MHz / 60 Hz (NTSC 262-line frame)
41
- const VsyncPulse = 800;
42
- let inVsync = false;
43
- const task = this.processor.scheduler.newTask(() => {
44
- if (!inVsync) {
45
- ppia.setVBlankInt(1);
46
- inVsync = true;
47
- task.reschedule(VsyncPulse);
48
- } else {
49
- ppia.setVBlankInt(0);
50
- inVsync = false;
51
- task.reschedule(VsyncPeriod - VsyncPulse);
52
- }
53
- });
54
- task.schedule(VsyncPeriod);
55
- }
56
-
57
- /**
58
- * Install the character capture hook (once). All characters sent
59
- * through WRCHV are accumulated and can be read with drainText().
60
- * Safe to call multiple times — only installs one hook.
61
- */
62
- startCapture() {
63
- if (this._captureHookInstalled) return;
64
- this._captureHookInstalled = true;
65
- // WRCHV is at 0x0208 on Atom, 0x020E on BBC.
66
- const wrchvAddr = this.model.isAtom ? 0x0208 : 0x020e;
67
- this.processor.debugInstruction.add((addr) => {
68
- const wrchv = this.readword(wrchvAddr);
69
- if (addr === wrchv) {
70
- this._capturedChars.push(this.processor.a);
71
- }
72
- return false;
73
- });
74
- }
75
-
76
- /**
77
- * Return all captured characters since the last drain (or since
78
- * startCapture was called), then clear the buffer.
79
- * @returns {number[]} array of character codes
80
- */
81
- drainCapturedChars() {
82
- const chars = this._capturedChars;
83
- this._capturedChars = [];
84
- return chars;
85
- }
86
-
87
- /**
88
- * Return captured text as a string (printable chars only, with
89
- * optional newline preservation), then clear the buffer.
90
- * @param {Object} [opts]
91
- * @param {boolean} [opts.raw=false] - if true, preserve newlines
92
- */
93
- drainText({ raw = false } = {}) {
94
- const chars = this.drainCapturedChars();
95
- return chars
96
- .map((c) => {
97
- if (raw && c === 10) return "\n";
98
- if (c === 13) return "";
99
- if (c >= 0x20 && c < 0x7f) return String.fromCharCode(c);
100
- return "";
101
- })
102
- .join("");
103
- }
104
-
105
- runFor(cycles) {
106
- let left = cycles;
107
- let stopped = false;
108
- return new Promise((resolve) => {
109
- const runAnIter = () => {
110
- const todo = Math.max(0, Math.min(left, MaxCyclesPerIter));
111
- if (todo) {
112
- stopped = !this.processor.execute(todo);
113
- left -= todo;
114
- }
115
- // Not truthiness: a negative or NaN request clamps todo to zero,
116
- // so left would never move and the loop never end.
117
- if (left > 0 && !stopped) {
118
- setTimeout(runAnIter, 0);
119
- } else {
120
- resolve(stopped);
121
- }
122
- };
123
- runAnIter();
124
- });
125
- }
126
-
127
- /**
128
- * Run until the cursor blink reaches the desired state.
129
- * This ensures deterministic screenshots regardless of how many
130
- * cycles were consumed by prior type() or runFor() calls.
131
- * @param {boolean} on - true for cursor visible, false for hidden
132
- */
133
- async runToCursorState(on) {
134
- const video = this.processor.video;
135
- for (let i = 0; i < 100; i++) {
136
- if (video.cursorOnThisFrame === on) return;
137
- await this.runFor(40000);
138
- }
139
- throw new Error(`Cursor did not reach state ${on} in time (cursorOnThisFrame=${video.cursorOnThisFrame})`);
140
- }
141
-
142
- async runUntilFlashHidden() {
143
- const teletext = this.processor.video.teletext;
144
- for (let i = 0; i < 100; i++) {
145
- if (teletext.hideFlashing) return;
146
- await this.runFor(40000);
147
- }
148
- throw new Error("Flashing text did not reach its hidden phase in time");
149
- }
150
-
151
- async runUntilVblank() {
152
- let hit = false;
153
- if (this.processor.isMaster) throw new Error("Not yet implemented");
154
- const hook = this.processor.debugInstruction.add((addr) => {
155
- if (addr === 0xdd15) {
156
- hit = true;
157
- return true;
158
- }
159
- });
160
- await this.runFor(10 * 1000 * 1000);
161
- hook.remove();
162
- assert(hit, "did not hit appropriate breakpoint in time");
163
- }
164
-
165
- async runUntilInput(secs) {
166
- if (!secs) secs = 120;
167
- console.log("Running until keyboard input requested");
168
- if (this.model.isAtom) {
169
- // The Atom kernel's keyboard read loop at $FE94 is entered when
170
- // BASIC (or the OS) waits for a keypress. We detect entry to
171
- // this routine as the idle point.
172
- const atomIdleAddr = 0xfe94;
173
- let hit = false;
174
- const hook = this.processor.debugInstruction.add((addr) => {
175
- if (addr === atomIdleAddr) {
176
- hit = true;
177
- return true;
178
- }
179
- });
180
- await this.runFor(secs * this.model.cyclesPerSecond);
181
- hook.remove();
182
- assert(hit, "Atom did not reach keyboard input in time");
183
- return this.runFor(10 * 1000);
184
- }
185
- const idleAddr = basicIdleAddr(this.processor.model);
186
- let hit = false;
187
- const hook = this.processor.debugInstruction.add((addr) => {
188
- if (addr === idleAddr) {
189
- hit = true;
190
- return true;
191
- }
192
- });
193
- await this.runFor(secs * this.model.cyclesPerSecond);
194
- hook.remove();
195
- assert(hit, "did not hit appropriate breakpoint in time");
196
- return this.runFor(10 * 1000);
197
- }
198
-
199
- async runUntilAddress(targetAddr, secs) {
200
- if (!secs) secs = 120;
201
- let hit = false;
202
- const hook = this.processor.debugInstruction.add((addr) => {
203
- if (addr === targetAddr) {
204
- hit = true;
205
- return true;
206
- }
207
- });
208
- await this.runFor(secs * this.model.cyclesPerSecond);
209
- hook.remove();
210
- assert(hit, "did not hit appropriate breakpoint in time");
211
- }
212
-
213
- async loadDisc(image) {
214
- const data = await fdc.load(image);
215
- this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc, image, data));
216
- }
217
-
218
- /**
219
- * Load a disc image from raw data (Uint8Array or Buffer).
220
- * @param {Uint8Array|Buffer} data - raw disc image bytes
221
- */
222
- loadDiscData(data) {
223
- this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc, "", data));
224
- }
225
-
226
- /**
227
- * Reset the machine.
228
- * @param {boolean} hard - true for power-on reset, false for soft reset
229
- */
230
- reset(hard) {
231
- this.processor.reset(hard);
232
- }
233
-
234
- /**
235
- * Take a snapshot of the entire machine state (CPU, RAM, SWRAM,
236
- * VIAs, video, FDC, etc). Returns an opaque state object that
237
- * can be passed to restore().
238
- */
239
- snapshot({ includeRoms = true } = {}) {
240
- return this.processor.snapshotState({ includeRoms });
241
- }
242
-
243
- /**
244
- * Restore a previously saved snapshot. The machine will be in
245
- * exactly the state it was when snapshot() was called.
246
- */
247
- restore(state) {
248
- this.processor.restoreState(state);
249
- }
250
-
251
- async loadBasic(source) {
252
- const tokeniser = await Tokeniser.create();
253
- const tokenised = tokeniser.tokenise(source);
254
- installBasic(tokenised, {
255
- readByte: (addr) => this.readbyte(addr),
256
- writeByte: (addr, value) => this.writebyte(addr, value),
257
- });
258
- }
259
-
260
- /**
261
- * Convert an ASCII character to a {code, shift} pair for the BBC keyboard.
262
- */
263
- _charToKey(ch) {
264
- switch (ch) {
265
- case "\n":
266
- case "\r":
267
- return { code: 13, shift: false };
268
- case '"':
269
- return { code: utils.keyCodes.K2, shift: true };
270
- case "*":
271
- return { code: utils.keyCodes.APOSTROPHE, shift: true };
272
- case "!":
273
- return { code: utils.keyCodes.K1, shift: true };
274
- case ".":
275
- return { code: utils.keyCodes.PERIOD, shift: false };
276
- case ";":
277
- return { code: utils.keyCodes.SEMICOLON, shift: false };
278
- case ":":
279
- return { code: utils.keyCodes.APOSTROPHE, shift: false };
280
- case ",":
281
- return { code: utils.keyCodes.COMMA, shift: false };
282
- case "&":
283
- return { code: utils.keyCodes.K6, shift: true };
284
- case " ":
285
- return { code: utils.keyCodes.SPACE, shift: false };
286
- case "-":
287
- return { code: utils.keyCodes.MINUS, shift: false };
288
- case "=":
289
- return { code: utils.keyCodes.MINUS, shift: true };
290
- case "+":
291
- return { code: utils.keyCodes.SEMICOLON, shift: true };
292
- case "^":
293
- return { code: utils.keyCodes.EQUALS, shift: false };
294
- case "~":
295
- return { code: utils.keyCodes.EQUALS, shift: true };
296
- case "[":
297
- return { code: utils.keyCodes.LEFT_SQUARE_BRACKET, shift: false };
298
- case "]":
299
- return { code: utils.keyCodes.RIGHT_SQUARE_BRACKET, shift: false };
300
- case "{":
301
- return { code: utils.keyCodes.LEFT_SQUARE_BRACKET, shift: true };
302
- case "}":
303
- return { code: utils.keyCodes.HASH, shift: true };
304
- case "\\":
305
- return { code: utils.keyCodes.BACKSLASH, shift: false };
306
- case "/":
307
- return { code: utils.keyCodes.SLASH, shift: false };
308
- case "?":
309
- return { code: utils.keyCodes.SLASH, shift: true };
310
- case "<":
311
- return { code: utils.keyCodes.COMMA, shift: true };
312
- case ">":
313
- return { code: utils.keyCodes.PERIOD, shift: true };
314
- case "(":
315
- return { code: utils.keyCodes.K8, shift: true };
316
- case ")":
317
- return { code: utils.keyCodes.K9, shift: true };
318
- case "@":
319
- return { code: utils.keyCodes.BACK_QUOTE, shift: false };
320
- case "#":
321
- return { code: utils.keyCodes.K3, shift: true };
322
- case "$":
323
- return { code: utils.keyCodes.K4, shift: true };
324
- case "%":
325
- return { code: utils.keyCodes.K5, shift: true };
326
- default: {
327
- const upper = ch.toUpperCase();
328
- const isLetter = (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
329
- if (isLetter) {
330
- const wantUpper = ch >= "A" && ch <= "Z";
331
- const capsOn = this.processor.sysvia.capsLockLight;
332
- // CAPS LOCK on: unshifted = upper, shifted = lower
333
- // CAPS LOCK off: unshifted = lower, shifted = upper
334
- const needShift = capsOn ? !wantUpper : wantUpper;
335
- return { code: upper.charCodeAt(0), shift: needShift };
336
- }
337
- return { code: ch.charCodeAt(0), shift: false };
338
- }
339
- }
340
- }
341
-
342
- /**
343
- * Type text by installing a debugInstruction hook that presses/releases
344
- * keys at timed intervals during CPU execution. The hook persists across
345
- * runFor calls, so breakpoints naturally coexist: if a breakpoint halts
346
- * execution mid-typing, the remaining characters are typed when execution
347
- * resumes.
348
- */
349
- async type(text) {
350
- if (this.model.isAtom) {
351
- return this._typeAtom(text);
352
- }
353
- const fullText = text + "\n"; // append RETURN
354
- const keys = fullText.split("").map((ch) => this._charToKey(ch));
355
- // Key hold is counted in CPU cycles, so scale it to keep the hold constant in
356
- // real time: the OS scans the keyboard on a peripheral-rate interrupt.
357
- const holdCycles = (40000 * this.processor.cpuMultiplier) | 0;
358
- let index = 0;
359
- let phase = "idle"; // "idle" → "down" → "idle"
360
- let nextEventCycle = 0;
361
- let done = false;
362
-
363
- const currentCycle = () =>
364
- this.processor.cycleSeconds * this.model.cyclesPerSecond + this.processor.currentCycles;
365
-
366
- const hook = this.processor.debugInstruction.add(() => {
367
- if (currentCycle() < nextEventCycle) return;
368
-
369
- if (phase === "down") {
370
- // Release current key
371
- const key = keys[index];
372
- this.processor.sysvia.keyUp(key.code);
373
- if (key.shift) this.processor.sysvia.keyUp(16);
374
- index++;
375
- phase = "idle";
376
- nextEventCycle = currentCycle() + holdCycles;
377
- return;
378
- }
379
-
380
- // phase === "idle"
381
- if (index >= keys.length) {
382
- hook.remove();
383
- done = true;
384
- return;
385
- }
386
-
387
- // Press next key
388
- const key = keys[index];
389
- if (key.shift) this.processor.sysvia.keyDown(16);
390
- this.processor.sysvia.keyDown(key.code);
391
- phase = "down";
392
- nextEventCycle = currentCycle() + holdCycles;
393
- });
394
-
395
- // Drive execution in chunks until all characters are typed or
396
- // a breakpoint halts the CPU.
397
- while (!done) {
398
- const stopped = await this.runFor(holdCycles);
399
- if (stopped) break;
400
- }
401
- }
402
-
403
- /** Type text on the Atom using its key mapping and PPIA interface. */
404
- async _typeAtom(text) {
405
- // stringToATOMKeys returns a flat array of [col, row] pairs.
406
- // SHIFT is held across multiple characters; LOCK is tapped to
407
- // toggle the ROM's internal caps lock state.
408
- const keySequence = utils_atom.stringToATOMKeys(text + "\n");
409
- const ppia = this.processor.atomppia;
410
- const holdCycles = (80000 * this.processor.cpuMultiplier) | 0; // Atom at 1 MHz needs longer hold than BBC at 2 MHz
411
- const SHIFT = utils_atom.ATOM.SHIFT;
412
-
413
- let index = 0;
414
- let phase = "idle";
415
- let nextEventCycle = 0;
416
- let done = false;
417
- let shiftHeld = false;
418
-
419
- const currentCycle = () =>
420
- this.processor.cycleSeconds * this.model.cyclesPerSecond + this.processor.currentCycles;
421
-
422
- const isShift = (entry) => entry[0] === SHIFT[0] && entry[1] === SHIFT[1];
423
-
424
- const hook = this.processor.debugInstruction.add(() => {
425
- if (currentCycle() < nextEventCycle) return;
426
-
427
- if (phase === "down") {
428
- const entry = keySequence[index];
429
- if (!isShift(entry)) {
430
- ppia.keyUpRaw(entry);
431
- }
432
- index++;
433
- phase = "idle";
434
- nextEventCycle = currentCycle() + holdCycles;
435
- return;
436
- }
437
-
438
- if (index >= keySequence.length) {
439
- if (shiftHeld) ppia.keyUpRaw(SHIFT);
440
- hook.remove();
441
- done = true;
442
- return;
443
- }
444
-
445
- const entry = keySequence[index];
446
- if (isShift(entry)) {
447
- if (shiftHeld) {
448
- ppia.keyUpRaw(SHIFT);
449
- shiftHeld = false;
450
- } else {
451
- ppia.keyDownRaw(SHIFT);
452
- shiftHeld = true;
453
- }
454
- index++;
455
- nextEventCycle = currentCycle() + holdCycles;
456
- } else {
457
- ppia.keyDownRaw(entry);
458
- phase = "down";
459
- nextEventCycle = currentCycle() + holdCycles;
460
- }
461
- });
462
-
463
- while (!done) {
464
- const stopped = await this.runFor(holdCycles);
465
- if (stopped) {
466
- hook.remove();
467
- if (shiftHeld) ppia.keyUpRaw(SHIFT);
468
- break;
469
- }
470
- }
471
- }
472
-
473
- /**
474
- * Press a key on the keyboard.
475
- * @param {number} code - key code (BBC keyCode or Atom raw key)
476
- */
477
- keyDown(code) {
478
- this._keyInterface.keyDown(code);
479
- }
480
-
481
- /**
482
- * Release a key on the keyboard.
483
- * @param {number} code - key code
484
- */
485
- keyUp(code) {
486
- this._keyInterface.keyUp(code);
487
- }
488
-
489
- /**
490
- * Load a ROM image directly into a sideways RAM slot.
491
- * @param {number} slot - slot number (0-15, typically 4-7 for SWRAM)
492
- * @param {Uint8Array|Buffer} data - ROM data (up to 16384 bytes)
493
- */
494
- loadSidewaysRam(slot, data) {
495
- const offset = this.processor.romOffset + slot * 16384;
496
- for (let i = 0; i < data.length && i < 16384; i++) {
497
- this.processor.ramRomOs[offset + i] = data[i];
498
- }
499
- }
500
-
501
- writebyte(addr, val) {
502
- this.processor.writemem(addr, val);
503
- }
504
-
505
- readbyte(addr) {
506
- return this.processor.readmem(addr);
507
- }
508
-
509
- readword(addr) {
510
- return this.readbyte(addr) | (this.readbyte(addr + 1) << 8);
511
- }
512
-
513
- captureText(onElement) {
514
- const attributes = {
515
- x: 0,
516
- y: 0,
517
- text: "",
518
- foreground: 7,
519
- background: 0,
520
- mode: 7,
521
- };
522
- let currentText = "";
523
- let params = [];
524
- let nextN = 0;
525
- let vduProc = null;
526
-
527
- function flush() {
528
- if (currentText.length) {
529
- attributes.text = currentText;
530
- onElement(attributes);
531
- attributes.x += currentText.length; // Approximately...anyway
532
- }
533
- currentText = "";
534
- }
535
-
536
- function onChar(c) {
537
- if (nextN) {
538
- params.push(c);
539
- if (--nextN === 0) {
540
- if (vduProc) vduProc(params);
541
- params = [];
542
- vduProc = null;
543
- }
544
- return;
545
- }
546
- switch (c) {
547
- case 1: // Next char to printer
548
- nextN = 1;
549
- break;
550
- case 10:
551
- attributes.y++;
552
- break;
553
- case 12: // CLS
554
- attributes.x = 0;
555
- attributes.y = 0;
556
- break;
557
- case 13:
558
- attributes.x = 0;
559
- break;
560
- case 17: // Text colour
561
- nextN = 1;
562
- vduProc = function (params) {
563
- if (params[0] & 0x80) attributes.background = params[0] & 0xf;
564
- else attributes.foreground = params[0] & 0xf;
565
- };
566
- break;
567
- case 18: // GCOL
568
- nextN = 2;
569
- break;
570
- case 19: // logical colour
571
- nextN = 5;
572
- break;
573
- case 22: // mode
574
- nextN = 1;
575
- vduProc = function (params) {
576
- attributes.mode = params[0];
577
- attributes.x = 0;
578
- attributes.y = 0;
579
- };
580
- break;
581
- case 25: // plot
582
- nextN = 5;
583
- break;
584
- case 28: // text window
585
- nextN = 4;
586
- break;
587
- case 29: // origin
588
- nextN = 4;
589
- break;
590
- case 31: // text location
591
- nextN = 2;
592
- vduProc = function (params) {
593
- attributes.x = params[0];
594
- attributes.y = params[1];
595
- };
596
- }
597
- if (c >= 32 && c < 0x7f) {
598
- currentText += String.fromCharCode(c);
599
- } else flush();
600
- return false;
601
- }
602
-
603
- const wrchv = this.readword(0x20e);
604
- this.processor.debugInstruction.add((addr) => {
605
- if (addr === wrchv) onChar(this.processor.a);
606
- return false;
607
- });
608
- }
609
- }