jsbeeb 1.23.0 → 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 (110) hide show
  1. package/README.md +2 -2
  2. package/package.json +14 -10
  3. package/src/6502.js +39 -42
  4. package/src/6502.opcodes.js +7 -13
  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/{utils_atom.js → keymap-atom.js} +2 -18
  31. package/src/{utils.js → keymap.js} +47 -610
  32. package/src/loader.js +63 -0
  33. package/src/machine-session.js +37 -177
  34. package/src/machine-spec.js +59 -0
  35. package/src/main.js +66 -180
  36. package/src/media-resolver.js +68 -0
  37. package/src/mmc.js +3 -2
  38. package/src/models.js +17 -3
  39. package/src/music5000.js +0 -2
  40. package/src/ppia.js +3 -4
  41. package/src/printer.js +7 -14
  42. package/src/relaynoise.js +0 -1
  43. package/src/rewind.js +0 -2
  44. package/src/sample-player.js +2 -3
  45. package/src/serial.js +0 -2
  46. package/src/snapshot-helpers.js +0 -2
  47. package/src/snapshot.js +0 -2
  48. package/src/soundchip.js +1 -1
  49. package/src/state-utils.js +0 -2
  50. package/src/sth.js +2 -4
  51. package/src/tapes.js +10 -4
  52. package/src/teletext.js +2 -3
  53. package/src/teletext_adaptor.js +2 -4
  54. package/src/teletext_data.js +0 -2
  55. package/src/test-machine.js +307 -0
  56. package/src/touchscreen.js +2 -4
  57. package/src/tube.js +5 -6
  58. package/src/typist.js +103 -0
  59. package/src/uef-snapshot.js +0 -2
  60. package/src/url-params.js +4 -5
  61. package/src/vdu-capture.js +113 -0
  62. package/src/via.js +3 -4
  63. package/src/video-filters/pal-composite.js +0 -2
  64. package/src/video-filters/passthrough-filter.js +0 -2
  65. package/src/video-filters/pixel-grid.js +0 -2
  66. package/src/video-filters/shader-program.js +0 -2
  67. package/src/video-filters/xbr-filter.js +0 -2
  68. package/src/video.js +12 -11
  69. package/src/wd-fdc.js +20 -20
  70. package/src/web/analogue-inputs.js +6 -8
  71. package/src/web/analytics.js +13 -0
  72. package/src/web/audio-handler.js +2 -2
  73. package/src/{audio-utils.js → web/audio-utils.js} +0 -2
  74. package/src/web/autoboot.js +10 -13
  75. package/src/{canvas.js → web/canvas.js} +4 -5
  76. package/src/web/config.js +41 -74
  77. package/src/web/console-surface.js +25 -0
  78. package/src/web/debug.js +6 -5
  79. package/src/web/disc-visualiser.js +0 -2
  80. package/src/web/display.js +21 -4
  81. package/src/{dom-utils.js → web/dom-utils.js} +1 -11
  82. package/src/web/drives.js +1 -1
  83. package/src/web/emulation-loop.js +54 -33
  84. package/src/web/front-panel.js +13 -12
  85. package/src/{gamepads.js → web/gamepads.js} +2 -6
  86. package/src/web/google-drive-picker.js +14 -9
  87. package/src/{google-drive.js → web/google-drive.js} +9 -10
  88. package/src/web/hfe-picker.js +5 -4
  89. package/src/web/keyboard-setup.js +18 -38
  90. package/src/{keyboard.js → web/keyboard.js} +18 -119
  91. package/src/web/layout.js +4 -0
  92. package/src/web/local-disc.js +44 -0
  93. package/src/web/machine.js +27 -20
  94. package/src/web/media-loader.js +38 -147
  95. package/src/{microphone-input.js → web/microphone-input.js} +1 -1
  96. package/src/web/modals.js +6 -9
  97. package/src/web/page-actions.js +88 -0
  98. package/src/web/quick-settings.js +16 -15
  99. package/src/web/reporting.js +0 -7
  100. package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
  101. package/src/web/rewind-ui.js +29 -29
  102. package/src/web/run-controls.js +39 -0
  103. package/src/web/settings.js +64 -135
  104. package/src/web/snapshot-ui.js +10 -16
  105. package/src/{speech-output.js → web/speech-output.js} +0 -2
  106. package/src/web/sth-picker.js +9 -7
  107. package/src/web/url-state.js +20 -6
  108. package/src/econet/L3FS.dat +0 -0
  109. package/src/econet/scsi.dat +0 -0
  110. package/tests/test-machine.js +0 -609
package/src/sth.js CHANGED
@@ -1,6 +1,4 @@
1
- "use strict";
2
-
3
- import * as utils from "./utils.js";
1
+ import { unzipDiscImage } from "./archive.js";
4
2
 
5
3
  // Always https, whatever the page was loaded over: the mirror redirects plain
6
4
  // http, so following the page's protocol would cost a redirect on every request
@@ -55,7 +53,7 @@ export class StairwayToHell {
55
53
  const response = await fetch(url);
56
54
  if (!response.ok) throw new Error(`Unable to load ${url}, http code ${response.status}`);
57
55
  try {
58
- return await utils.unzipDiscImage(new Uint8Array(await response.arrayBuffer()));
56
+ return await unzipDiscImage(new Uint8Array(await response.arrayBuffer()));
59
57
  } catch (error) {
60
58
  console.error("Failed to fetch file:", error);
61
59
  throw error;
package/src/tapes.js CHANGED
@@ -1,5 +1,6 @@
1
- "use strict";
2
- import * as utils from "./utils.js";
1
+ import { DataStream, stringToUint8Array } from "./binary.js";
2
+ import { ungzip } from "./archive.js";
3
+ import { hexword } from "./hex.js";
3
4
 
4
5
  function secsToClocks(secs, cpuSpeed) {
5
6
  return (cpuSpeed * secs) | 0;
@@ -240,7 +241,7 @@ class UefTape {
240
241
  acia.tone(0);
241
242
  return secsToClocks(gap, this.cpuSpeed);
242
243
  default:
243
- console.log("Skipping unknown chunk " + utils.hexword(this.curChunk.id));
244
+ console.log("Skipping unknown chunk " + hexword(this.curChunk.id));
244
245
  this.curChunk = this.readChunk();
245
246
  break;
246
247
  }
@@ -306,7 +307,12 @@ class TapefileTape {
306
307
  }
307
308
 
308
309
  export async function loadTapeFromData(name, data, model) {
309
- const stream = await utils.DataStream.create(name, data);
310
+ const raw = stringToUint8Array(data);
311
+ if (raw && raw.length > 4 && raw[0] === 0x1f && raw[1] === 0x8b) {
312
+ console.log("Ungzipping " + name);
313
+ data = await ungzip(raw);
314
+ }
315
+ const stream = new DataStream(name, data);
310
316
  if (stream.readByte(0) === 0xff && stream.readByte(1) === 0x04) {
311
317
  console.log("Detected a 'tapefile' tape");
312
318
  return new TapefileTape(stream, model);
package/src/teletext.js CHANGED
@@ -1,7 +1,6 @@
1
- "use strict";
2
1
  import { makeChars } from "./teletext_data.js";
3
- import { makeFast32 } from "./utils.js";
4
2
  import { BbcDefaultPalette as BbcDefaultCollook } from "./bbc-palette.js";
3
+ import { makeFast32 } from "./binary.js";
5
4
 
6
5
  export class Teletext {
7
6
  constructor() {
@@ -341,7 +340,7 @@ export class Teletext {
341
340
 
342
341
  // Flashing text starts off in sync with a slow cursor, extinguished together. Multiple MODE
343
342
  // changes gradually desynchronise the frame counters.
344
- // TODO: setDEW is reached a MOS-dependent number of times before Video.frameCount rises, so
343
+ // TODO(#1057) setDEW is reached a MOS-dependent number of times before Video.frameCount rises, so
345
344
  // the initial sync here holds under MOS 1.20 only.
346
345
  get hideFlashing() {
347
346
  return this.flashTime < 16;
@@ -1,6 +1,4 @@
1
- "use strict";
2
-
3
- import * as utils from "./utils.js";
1
+ import { loadData } from "./loader.js";
4
2
 
5
3
  // Code ported from Beebem (C to .js) by Jason Robson
6
4
  const TELETEXT_IRQ = 5;
@@ -74,7 +72,7 @@ export class TeletextAdaptor extends EventTarget {
74
72
  const request = ++this.streamRequest;
75
73
  let data;
76
74
  try {
77
- data = await utils.loadData(`teletext/txt${channel}.dat`);
75
+ data = await loadData(`teletext/txt${channel}.dat`);
78
76
  } catch (error) {
79
77
  if (request !== this.streamRequest) return;
80
78
  console.error(`Teletext adaptor: failed to load channel ${channel}`, error);
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  // Extremely heavily based on b-em.
4
2
  export function makeChars() {
5
3
  // prettier-ignore
@@ -0,0 +1,307 @@
1
+ import { installBasic } from "./basic-loader.js";
2
+ import * as fdc from "./fdc.js";
3
+ import { fake6502 } from "./fake6502.js";
4
+ import { findModel } from "./models.js";
5
+ import assert from "assert";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import * as Tokeniser from "./basic-tokenise.js";
9
+ import { VduTextCapture } from "./vdu-capture.js";
10
+ import { setNodeBasePath } from "./loader.js";
11
+ import { Typist } from "./typist.js";
12
+
13
+ const MaxCyclesPerIter = 100 * 1000;
14
+ const RepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+
16
+ export class TestMachine {
17
+ constructor(model, opts) {
18
+ model = model || "B-DFS1.2";
19
+ this.model = findModel(model);
20
+ if (!this.model) throw new Error(`Unknown model "${model}"`);
21
+ this.processor = fake6502(this.model, opts || {});
22
+ this.typist = new Typist(this.processor);
23
+ this._capturedChars = [];
24
+ this._captureHookInstalled = false;
25
+ }
26
+
27
+ /** The keyboard interface for this machine (SysVia for BBC, PPIA for Atom). */
28
+ get _keyInterface() {
29
+ return this.processor.keyboardInterface;
30
+ }
31
+
32
+ async initialise() {
33
+ setNodeBasePath(RepoRoot);
34
+ await this.processor.initialise();
35
+ if (this.model.isAtom) this._startAtomVSync();
36
+ }
37
+
38
+ /**
39
+ * The Atom ROM's main loop waits for VSync (bit 7 of Port C) to
40
+ * toggle before scanning the keyboard. The MC6847 video chip drives
41
+ * this in the real emulator, but fake6502 doesn't create one, so we
42
+ * simulate it with a scheduler task at ~60 Hz (NTSC).
43
+ */
44
+ _startAtomVSync() {
45
+ const ppia = this.processor.atomppia;
46
+ const VsyncPeriod = 16667; // 1 MHz / 60 Hz (NTSC 262-line frame)
47
+ const VsyncPulse = 800;
48
+ let inVsync = false;
49
+ const task = this.processor.scheduler.newTask(() => {
50
+ if (!inVsync) {
51
+ ppia.setVBlankInt(1);
52
+ inVsync = true;
53
+ task.reschedule(VsyncPulse);
54
+ } else {
55
+ ppia.setVBlankInt(0);
56
+ inVsync = false;
57
+ task.reschedule(VsyncPeriod - VsyncPulse);
58
+ }
59
+ });
60
+ task.schedule(VsyncPeriod);
61
+ }
62
+
63
+ /**
64
+ * Calls `listener` with each character the machine sends to the VDU, by
65
+ * watching the write-character vector (WRCHV, $020E on the BBC and $0208 on
66
+ * the Atom) as the OS or a program leaves it. Returns a function that stops
67
+ * listening.
68
+ */
69
+ onVduChar(listener) {
70
+ if (!this._vduListeners) {
71
+ this._vduListeners = [];
72
+ const cpu = this.processor;
73
+ const ram = cpu.ramRomOs;
74
+ const wrchvAddr = this.model.wrchvAddress;
75
+ cpu.debugInstruction.add((addr) => {
76
+ if (addr === (ram[wrchvAddr] | (ram[wrchvAddr + 1] << 8))) {
77
+ for (const listen of this._vduListeners) listen(cpu.a);
78
+ }
79
+ return false;
80
+ });
81
+ }
82
+ this._vduListeners.push(listener);
83
+ return () => {
84
+ this._vduListeners = this._vduListeners.filter((other) => other !== listener);
85
+ };
86
+ }
87
+
88
+ /** Accumulates every character sent to the VDU for drainText(); safe to call more than once. */
89
+ startCapture() {
90
+ if (this._captureHookInstalled) return;
91
+ this._captureHookInstalled = true;
92
+ this.onVduChar((c) => this._capturedChars.push(c));
93
+ }
94
+
95
+ /**
96
+ * Return all captured characters since the last drain (or since
97
+ * startCapture was called), then clear the buffer.
98
+ * @returns {number[]} array of character codes
99
+ */
100
+ drainCapturedChars() {
101
+ const chars = this._capturedChars;
102
+ this._capturedChars = [];
103
+ return chars;
104
+ }
105
+
106
+ /**
107
+ * Return captured text as a string (printable chars only, with
108
+ * optional newline preservation), then clear the buffer.
109
+ * @param {Object} [opts]
110
+ * @param {boolean} [opts.raw=false] - if true, preserve newlines
111
+ */
112
+ drainText({ raw = false } = {}) {
113
+ const chars = this.drainCapturedChars();
114
+ return chars
115
+ .map((c) => {
116
+ if (raw && c === 10) return "\n";
117
+ if (c === 13) return "";
118
+ if (c >= 0x20 && c < 0x7f) return String.fromCharCode(c);
119
+ return "";
120
+ })
121
+ .join("");
122
+ }
123
+
124
+ runFor(cycles) {
125
+ let left = cycles;
126
+ let stopped = false;
127
+ return new Promise((resolve) => {
128
+ const runAnIter = () => {
129
+ const todo = Math.max(0, Math.min(left, MaxCyclesPerIter));
130
+ if (todo) {
131
+ stopped = !this.processor.execute(todo);
132
+ left -= todo;
133
+ }
134
+ // Not truthiness: a negative or NaN request clamps todo to zero,
135
+ // so left would never move and the loop never end.
136
+ if (left > 0 && !stopped) {
137
+ setTimeout(runAnIter, 0);
138
+ } else {
139
+ resolve(stopped);
140
+ }
141
+ };
142
+ runAnIter();
143
+ });
144
+ }
145
+
146
+ /** Emulated cycles since power-on, undoing the per-second rebasing execute() applies. */
147
+ get elapsedCycles() {
148
+ const cpu = this.processor;
149
+ return cpu.cycleSeconds * this.model.cyclesPerSecond + cpu.currentCycles;
150
+ }
151
+
152
+ async runUntilVblank() {
153
+ let hit = false;
154
+ if (this.processor.isMaster) throw new Error("Not yet implemented");
155
+ const hook = this.processor.debugInstruction.add((addr) => {
156
+ if (addr === 0xdd15) {
157
+ hit = true;
158
+ return true;
159
+ }
160
+ });
161
+ await this.runFor(10 * 1000 * 1000);
162
+ hook.remove();
163
+ assert(hit, "did not hit appropriate breakpoint in time");
164
+ }
165
+
166
+ async runUntilInput(secs) {
167
+ if (!secs) secs = 120;
168
+ console.log("Running until keyboard input requested");
169
+ const idleAddr = this.model.idleAddress;
170
+ let hit = false;
171
+ const hook = this.processor.debugInstruction.add((addr) => {
172
+ if (addr === idleAddr) {
173
+ hit = true;
174
+ return true;
175
+ }
176
+ });
177
+ await this.runFor(secs * this.model.cyclesPerSecond);
178
+ hook.remove();
179
+ assert(hit, "did not hit appropriate breakpoint in time");
180
+ return this.runFor(10 * 1000);
181
+ }
182
+
183
+ async runUntilAddress(targetAddr, secs) {
184
+ if (!secs) secs = 120;
185
+ let hit = false;
186
+ const hook = this.processor.debugInstruction.add((addr) => {
187
+ if (addr === targetAddr) {
188
+ hit = true;
189
+ return true;
190
+ }
191
+ });
192
+ await this.runFor(secs * this.model.cyclesPerSecond);
193
+ hook.remove();
194
+ assert(hit, "did not hit appropriate breakpoint in time");
195
+ }
196
+
197
+ async loadDisc(image) {
198
+ const data = await fdc.load(image);
199
+ this.processor.fdc.loadDisc(0, fdc.discFor(image, data));
200
+ }
201
+
202
+ /**
203
+ * Load a disc image from raw data (Uint8Array or Buffer).
204
+ * @param {Uint8Array|Buffer} data - raw disc image bytes
205
+ */
206
+ loadDiscData(data) {
207
+ this.processor.fdc.loadDisc(0, fdc.discFor("", data));
208
+ }
209
+
210
+ /**
211
+ * Reset the machine.
212
+ * @param {boolean} hard - true for power-on reset, false for soft reset
213
+ */
214
+ reset(hard) {
215
+ this.processor.reset(hard);
216
+ }
217
+
218
+ /**
219
+ * Take a snapshot of the entire machine state (CPU, RAM, SWRAM,
220
+ * VIAs, video, FDC, etc). Returns an opaque state object that
221
+ * can be passed to restore().
222
+ */
223
+ snapshot({ includeRoms = true } = {}) {
224
+ return this.processor.snapshotState({ includeRoms });
225
+ }
226
+
227
+ /**
228
+ * Restore a previously saved snapshot. The machine will be in
229
+ * exactly the state it was when snapshot() was called.
230
+ */
231
+ restore(state) {
232
+ this.processor.restoreState(state);
233
+ }
234
+
235
+ async loadBasic(source) {
236
+ const tokeniser = await Tokeniser.create();
237
+ const tokenised = tokeniser.tokenise(source);
238
+ installBasic(tokenised, {
239
+ readByte: (addr) => this.readbyte(addr),
240
+ writeByte: (addr, value) => this.writebyte(addr, value),
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Types the text and a RETURN at the machine, running the CPU until the
246
+ * last key is up. The keys arrive from the scheduler, so a breakpoint that
247
+ * stops the CPU part way leaves the rest to be typed when it runs again.
248
+ */
249
+ async type(text) {
250
+ const lines = text.replace(/\r\n?/g, "\n");
251
+ this.typist.type(this.model.stringToKeys(lines + "\n"), true);
252
+ while (this.typist.isTyping) {
253
+ const stopped = await this.runFor(MaxCyclesPerIter);
254
+ if (stopped) break;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Press a key on the keyboard.
260
+ * @param {number} code - key code (BBC keyCode or Atom raw key)
261
+ */
262
+ keyDown(code) {
263
+ this._keyInterface.keyDown(code);
264
+ }
265
+
266
+ /**
267
+ * Release a key on the keyboard.
268
+ * @param {number} code - key code
269
+ */
270
+ keyUp(code) {
271
+ this._keyInterface.keyUp(code);
272
+ }
273
+
274
+ /**
275
+ * Load a ROM image directly into a sideways RAM slot.
276
+ * @param {number} slot - slot number (0-15, typically 4-7 for SWRAM)
277
+ * @param {Uint8Array|Buffer} data - ROM data (up to 16384 bytes)
278
+ */
279
+ loadSidewaysRam(slot, data) {
280
+ const offset = this.processor.romOffset + slot * 16384;
281
+ for (let i = 0; i < data.length && i < 16384; i++) {
282
+ this.processor.ramRomOs[offset + i] = data[i];
283
+ }
284
+ }
285
+
286
+ writebyte(addr, val) {
287
+ this.processor.writemem(addr, val);
288
+ }
289
+
290
+ readbyte(addr) {
291
+ return this.processor.readmem(addr);
292
+ }
293
+
294
+ readword(addr) {
295
+ return this.readbyte(addr) | (this.readbyte(addr + 1) << 8);
296
+ }
297
+
298
+ /**
299
+ * Decodes the machine's VDU output into text elements for `onElement`
300
+ * until the returned capture's `stop()` is called; see VduTextCapture.
301
+ */
302
+ captureText(onElement) {
303
+ const capture = new VduTextCapture(onElement, { isAtom: this.model.isAtom });
304
+ capture.stop = this.onVduChar((c) => capture.onChar(c));
305
+ return capture;
306
+ }
307
+ }
@@ -1,6 +1,4 @@
1
- "use strict";
2
-
3
- import * as utils from "./utils.js";
1
+ import { Fifo } from "./binary.js";
4
2
 
5
3
  const PollHz = 8; // Made up
6
4
 
@@ -13,7 +11,7 @@ export class TouchScreen {
13
11
  constructor(scheduler, cyclesPerSecond) {
14
12
  this.scheduler = scheduler;
15
13
  this.pollCycles = cyclesPerSecond / PollHz;
16
- this.outBuffer = new utils.Fifo(16);
14
+ this.outBuffer = new Fifo(16);
17
15
  this.pollTask = this.scheduler.newTask(() => this.poll());
18
16
  this.reset();
19
17
  }
package/src/tube.js CHANGED
@@ -1,5 +1,4 @@
1
- "use strict";
2
- import * as utils from "./utils.js";
1
+ import { hexbyte, hexword } from "./hex.js";
3
2
 
4
3
  // this one should be declared more globally
5
4
  const HOST_CPU_FLAG_IRQ_TUBE_ULA = 8;
@@ -205,13 +204,13 @@ export class Tube {
205
204
  }
206
205
  this.updateInterrupts();
207
206
  if (this.debug) {
208
- console.log("TUBE ULA: host read " + utils.hexword(address) + " = " + utils.hexbyte(result));
207
+ console.log("TUBE ULA: host read " + hexword(address) + " = " + hexbyte(result));
209
208
  }
210
209
  return result;
211
210
  }
212
211
  hostWrite(address, value) {
213
212
  if (this.debug) {
214
- console.log("TUBE ULA: host write " + utils.hexword(address) + " = " + utils.hexbyte(value));
213
+ console.log("TUBE ULA: host write " + hexword(address) + " = " + hexbyte(value));
215
214
  }
216
215
  switch (address & 7) {
217
216
  case TUBE_ULA_R1_STATUS_ADDRESS: {
@@ -337,7 +336,7 @@ export class Tube {
337
336
  }
338
337
  this.updateInterrupts();
339
338
  if (this.debug) {
340
- console.log("TUBE ULA: parasite read " + utils.hexword(address) + " = " + utils.hexbyte(result));
339
+ console.log("TUBE ULA: parasite read " + hexword(address) + " = " + hexbyte(result));
341
340
  }
342
341
  return result;
343
342
  }
@@ -382,7 +381,7 @@ export class Tube {
382
381
  // Boot mode is terminated by the software when it selects any one of the Tube addresses.
383
382
  // This deselects the ROM
384
383
  if (this.debug) {
385
- console.log("TUBE ULA: parasite write " + utils.hexword(address) + " = " + utils.hexbyte(value));
384
+ console.log("TUBE ULA: parasite write " + hexword(address) + " = " + hexbyte(value));
386
385
  }
387
386
  switch (address & 7) {
388
387
  case TUBE_ULA_R1_DATA_ADDRESS:
package/src/typist.js ADDED
@@ -0,0 +1,103 @@
1
+ import { BBC } from "./keymap.js";
2
+
3
+ const RepeatedKeyGapMs = 30;
4
+
5
+ /**
6
+ * Types a key sequence into the keyboard matrix from the processor's
7
+ * scheduler, one key per tick, so the CPU stays on its fast path while the
8
+ * text arrives. An entry is one of the model's raw keys, or a number of
9
+ * milliseconds to wait. SHIFT toggles and stays held; any other key is pressed
10
+ * on one tick and released on the next.
11
+ */
12
+ export class Typist {
13
+ constructor(processor) {
14
+ this.processor = processor;
15
+ this.keyInterface = processor.keyboardInterface;
16
+ this._shiftKey = processor.model.keys.SHIFT;
17
+ this._keys = [];
18
+ this._lastKey = undefined;
19
+ this._clocksPerMs = 0;
20
+ this._task = processor.scheduler.newTask(() => this._deliver());
21
+ }
22
+
23
+ /**
24
+ * @param {Array} keys raw keys and millisecond delays, consumed as they are sent
25
+ * @param {boolean} checkCapsAndShiftLocks bracket the keys with a lock toggle when
26
+ * the machine's lock lights say the letters would come out in the wrong case
27
+ */
28
+ type(keys, checkCapsAndShiftLocks) {
29
+ if (this.isTyping) this.cancel();
30
+ this.keyInterface.disableKeyboard();
31
+ // The task lives on the processor's scheduler, which is polled with peripheral
32
+ // cycles, so the delays stay in real time whatever the CPU multiplier is.
33
+ this._clocksPerMs = this.processor.peripheralCyclesPerSecond / 1000;
34
+
35
+ if (checkCapsAndShiftLocks) {
36
+ let toggleKey = null;
37
+ if (!this.keyInterface.capsLockLight) toggleKey = BBC.CAPSLOCK;
38
+ else if (this.keyInterface.shiftLockLight) toggleKey = BBC.SHIFTLOCK;
39
+ if (toggleKey) {
40
+ keys.unshift(toggleKey);
41
+ keys.push(toggleKey);
42
+ }
43
+ }
44
+
45
+ this._keys = keys;
46
+ this._lastKey = undefined;
47
+ this._task.schedule(0);
48
+ }
49
+
50
+ cancel() {
51
+ if (!this.isTyping) return;
52
+ this._task.cancel();
53
+ this._releaseLastKey();
54
+ this._lastKey = undefined;
55
+ this._keys = [];
56
+ this.keyInterface.enableKeyboard();
57
+ }
58
+
59
+ get isTyping() {
60
+ return this._keys.length > 0 || this._task.scheduled();
61
+ }
62
+
63
+ _releaseLastKey() {
64
+ if (this._lastKey && this._lastKey !== this._shiftKey) this.keyInterface.keyToggleRaw(this._lastKey);
65
+ }
66
+
67
+ _deliver() {
68
+ this._releaseLastKey();
69
+
70
+ if (this._keys.length === 0) {
71
+ this._lastKey = undefined;
72
+ this.keyInterface.enableKeyboard();
73
+ return;
74
+ }
75
+
76
+ const releaseGapMs = this.processor.model.pasteReleaseGapMs;
77
+ if (this._lastKey && this._lastKey !== this._shiftKey && releaseGapMs) {
78
+ this._lastKey = undefined;
79
+ this._task.schedule(releaseGapMs * this._clocksPerMs);
80
+ return;
81
+ }
82
+
83
+ const key = this._keys[0];
84
+ const repeated = this._lastKey === key;
85
+ this._lastKey = key;
86
+ if (repeated) {
87
+ this._lastKey = undefined;
88
+ this._task.schedule(RepeatedKeyGapMs * this._clocksPerMs);
89
+ return;
90
+ }
91
+
92
+ let delayMs = this.processor.model.pasteKeyDelayMs;
93
+ if (typeof key === "number") {
94
+ delayMs = key;
95
+ this._lastKey = undefined;
96
+ } else {
97
+ this.keyInterface.keyToggleRaw(key);
98
+ }
99
+
100
+ this._keys.shift();
101
+ this._task.schedule(delayMs * this._clocksPerMs);
102
+ }
103
+ }
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  // BeebEm UEF save state parser.
4
2
  // Converts a BeebEm UEF save state file into a jsbeeb snapshot object, in the same way
5
3
  // that bem-snapshot.js converts B-em snapshots.
package/src/url-params.js CHANGED
@@ -295,17 +295,16 @@ export function guessModelFromHostname(hostname) {
295
295
  }
296
296
 
297
297
  /**
298
- * Parse disc or tape images from the query parameters
298
+ * Parse disc images from the query parameters
299
299
  * @param {Object} parsedQuery - The query parameters
300
- * @returns {Object} Object containing disc and tape information
300
+ * @returns {Object} Object containing disc information
301
301
  * - discImage: disc image URL (?disc= or ?disc1=)
302
302
  * - secondDiscImage: second disc URL (?disc2=)
303
- * - tapeImage: tape image URL (?tape=)
304
303
  * - mmcImage: MMC/SD card image URL (?mmc=, Atom only)
305
304
  */
306
305
  export function parseMediaParams(parsedQuery) {
307
- const { disc, disc1, disc2, tape, mmc } = parsedQuery;
306
+ const { disc, disc1, disc2, mmc } = parsedQuery;
308
307
  const discImage = disc || disc1;
309
308
 
310
- return { discImage, secondDiscImage: disc2, tapeImage: tape, mmcImage: mmc };
309
+ return { discImage, secondDiscImage: disc2, mmcImage: mmc };
311
310
  }