jsbeeb 1.23.0 → 1.24.1
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 +2 -2
- package/package.json +14 -10
- package/src/6502.js +39 -42
- package/src/6502.opcodes.js +7 -13
- package/src/6847.js +4 -5
- package/src/6847_fontdata.js +0 -2
- package/src/acia.js +2 -3
- package/src/app/app.js +0 -1
- package/src/app/args.js +0 -2
- package/src/app/electron.js +4 -6
- package/src/app-bench.js +1 -3
- package/src/archive.js +243 -0
- package/src/basic-loader.js +0 -7
- package/src/basic-tokenise.js +19 -6
- package/src/bbc-palette.js +0 -2
- package/src/bbcdiscs.js +0 -2
- package/src/bem-snapshot.js +1 -3
- package/src/binary.js +175 -0
- package/src/cmos.js +0 -2
- package/src/ddnoise.js +0 -1
- package/src/debounce.js +7 -0
- package/src/disc-surface.js +0 -2
- package/src/disc.js +10 -9
- package/src/fake6502.js +11 -24
- package/src/fdc.js +9 -51
- package/src/filestore.js +10 -17
- package/src/hex.js +38 -0
- package/src/index.js +11 -0
- package/src/intel-fdc.js +18 -20
- package/src/{utils_atom.js → keymap-atom.js} +2 -18
- package/src/{utils.js → keymap.js} +47 -610
- package/src/loader.js +63 -0
- package/src/machine-session.js +37 -177
- package/src/machine-spec.js +59 -0
- package/src/main.js +66 -180
- package/src/media-resolver.js +68 -0
- package/src/mmc.js +3 -2
- package/src/models.js +18 -3
- package/src/music5000.js +0 -2
- package/src/ppia.js +3 -4
- package/src/printer.js +7 -14
- package/src/relaynoise.js +0 -1
- package/src/rewind.js +0 -2
- package/src/sample-player.js +2 -3
- package/src/serial.js +0 -2
- package/src/snapshot-helpers.js +0 -2
- package/src/snapshot.js +0 -2
- package/src/soundchip.js +1 -1
- package/src/state-utils.js +0 -2
- package/src/sth.js +2 -4
- package/src/tapes.js +10 -4
- package/src/teletext.js +2 -3
- package/src/teletext_adaptor.js +2 -4
- package/src/teletext_data.js +0 -2
- package/src/test-machine.js +281 -0
- package/src/touchscreen.js +2 -4
- package/src/tube.js +5 -6
- package/src/typist.js +103 -0
- package/src/uef-snapshot.js +0 -2
- package/src/url-params.js +4 -5
- package/src/vdu-capture.js +113 -0
- package/src/via.js +3 -4
- package/src/video-filters/pal-composite.js +0 -2
- package/src/video-filters/passthrough-filter.js +0 -2
- package/src/video-filters/pixel-grid.js +0 -2
- package/src/video-filters/shader-program.js +0 -2
- package/src/video-filters/xbr-filter.js +0 -2
- package/src/video.js +38 -11
- package/src/wd-fdc.js +20 -20
- package/src/web/analogue-inputs.js +6 -8
- package/src/web/analytics.js +13 -0
- package/src/web/audio-handler.js +2 -2
- package/src/{audio-utils.js → web/audio-utils.js} +0 -2
- package/src/web/autoboot.js +10 -13
- package/src/{canvas.js → web/canvas.js} +4 -5
- package/src/web/config.js +41 -74
- package/src/web/console-surface.js +25 -0
- package/src/web/debug.js +6 -5
- package/src/web/disc-visualiser.js +0 -2
- package/src/web/display.js +21 -4
- package/src/{dom-utils.js → web/dom-utils.js} +1 -11
- package/src/web/drives.js +1 -1
- package/src/web/emulation-loop.js +54 -33
- package/src/web/front-panel.js +13 -12
- package/src/{gamepads.js → web/gamepads.js} +2 -6
- package/src/web/google-drive-picker.js +14 -9
- package/src/{google-drive.js → web/google-drive.js} +9 -10
- package/src/web/hfe-picker.js +5 -4
- package/src/web/keyboard-setup.js +18 -38
- package/src/{keyboard.js → web/keyboard.js} +18 -119
- package/src/web/layout.js +4 -0
- package/src/web/local-disc.js +44 -0
- package/src/web/machine.js +27 -20
- package/src/web/media-loader.js +38 -147
- package/src/{microphone-input.js → web/microphone-input.js} +1 -1
- package/src/web/modals.js +6 -9
- package/src/web/page-actions.js +88 -0
- package/src/web/quick-settings.js +16 -15
- package/src/web/reporting.js +0 -7
- package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
- package/src/web/rewind-ui.js +29 -29
- package/src/web/run-controls.js +39 -0
- package/src/web/settings.js +64 -135
- package/src/web/snapshot-ui.js +10 -16
- package/src/{speech-output.js → web/speech-output.js} +0 -2
- package/src/web/sth-picker.js +9 -7
- package/src/web/url-state.js +20 -6
- package/src/econet/L3FS.dat +0 -0
- package/src/econet/scsi.dat +0 -0
- package/tests/test-machine.js +0 -609
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { debounce
|
|
4
|
-
import { discFor } from "./fdc.js";
|
|
1
|
+
import { discFor } from "../fdc.js";
|
|
2
|
+
import { uint8ArrayToString } from "../binary.js";
|
|
3
|
+
import { debounce } from "../debounce.js";
|
|
5
4
|
|
|
6
5
|
const MIME_TYPE = "application/vnd.jsbeeb.disc-image";
|
|
7
6
|
const CLIENT_ID = "356883185894-bhim19837nroivv18p0j25gecora60r5.apps.googleusercontent.com";
|
|
@@ -138,14 +137,14 @@ export class GoogleDriveLoader {
|
|
|
138
137
|
});
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
async create(
|
|
140
|
+
async create(name, data) {
|
|
142
141
|
console.log(`Google Drive: creating disc image: '${name}'`);
|
|
143
142
|
const response = await this.saveFile(name, data);
|
|
144
143
|
const meta = response.result;
|
|
145
|
-
return { fileId: meta.id, disc: this.makeDisc(
|
|
144
|
+
return { fileId: meta.id, disc: this.makeDisc(data, meta) };
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
makeDisc(
|
|
147
|
+
makeDisc(data, meta, layout) {
|
|
149
148
|
let flusher = null;
|
|
150
149
|
const name = meta.name;
|
|
151
150
|
const id = meta.id;
|
|
@@ -159,12 +158,12 @@ export class GoogleDriveLoader {
|
|
|
159
158
|
} else {
|
|
160
159
|
console.log("Making read-only disc");
|
|
161
160
|
}
|
|
162
|
-
return discFor(
|
|
161
|
+
return discFor(name, data, flusher, layout);
|
|
163
162
|
}
|
|
164
163
|
|
|
165
|
-
async load(
|
|
164
|
+
async load(fileId, layout) {
|
|
166
165
|
const meta = (await this.driveClient.files.get({ fileId, fields: FILE_FIELDS })).result;
|
|
167
166
|
const data = (await this.driveClient.files.get({ fileId, alt: "media" })).body;
|
|
168
|
-
return this.makeDisc(
|
|
167
|
+
return this.makeDisc(data, meta, layout);
|
|
169
168
|
}
|
|
170
169
|
}
|
package/src/web/hfe-picker.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as utils from "../utils.js";
|
|
2
1
|
import * as bootstrap from "bootstrap";
|
|
3
2
|
import { BbcDiscArchive, Provenance, describe as describeHfe, matches, provenancesIn } from "../bbcdiscs.js";
|
|
4
3
|
import { errorText } from "./reporting.js";
|
|
5
4
|
import { clearArchiveList, showArchiveMessage } from "./archive-list.js";
|
|
5
|
+
import { noteEvent } from "./analytics.js";
|
|
6
6
|
|
|
7
7
|
const HfeProvenanceLabels = {
|
|
8
8
|
[Provenance.Captured]: ["Captured", "Direct from disc"],
|
|
@@ -57,15 +57,16 @@ export class HfePicker {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async pick(file) {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
noteEvent("hfe", "click", file.path);
|
|
61
|
+
const image = "hfe:" + file.path;
|
|
62
|
+
this.media.setDisc1Image(image);
|
|
62
63
|
const needsAutoboot = this.urlState.params.autoboot !== undefined;
|
|
63
64
|
if (needsAutoboot) this.processor.reset(true);
|
|
64
65
|
|
|
65
66
|
const name = describeHfe(file).title;
|
|
66
67
|
this.modals.popupLoading("Loading " + name);
|
|
67
68
|
try {
|
|
68
|
-
const loaded = await this.media.loadDiscImage(
|
|
69
|
+
const loaded = await this.media.loadDiscImage(image, this.drives.layoutForDrive(0));
|
|
69
70
|
this.drives.putDiscIn(0, loaded);
|
|
70
71
|
this.modals.loadingFinished();
|
|
71
72
|
if (needsAutoboot) this.autoboot(name);
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Keyboard } from "../keyboard.js";
|
|
1
|
+
import { Keyboard } from "./keyboard.js";
|
|
3
2
|
import { showNotice } from "./reporting.js";
|
|
3
|
+
import { noteEvent } from "./analytics.js";
|
|
4
|
+
import { keyCodes } from "../keymap.js";
|
|
4
5
|
|
|
5
6
|
const PasteBoxId = "paste-text";
|
|
6
7
|
const TypingTargets = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Builds the emulated keyboard and wires the browser's shortcuts around it,
|
|
11
|
+
* exposing it as `keyboard` for whoever needs the machine's keys.
|
|
12
|
+
*/
|
|
9
13
|
export class KeyboardSetup {
|
|
10
14
|
/**
|
|
11
15
|
* @param {object} opts
|
|
@@ -26,28 +30,28 @@ export class KeyboardSetup {
|
|
|
26
30
|
keyboard.addEventListener("resume", () => actions.resume());
|
|
27
31
|
keyboard.addEventListener("break", (e) => {
|
|
28
32
|
// F12/Break: Reset processor
|
|
29
|
-
if (e.detail)
|
|
33
|
+
if (e.detail) noteEvent("keyboard", "press", "break");
|
|
30
34
|
});
|
|
31
35
|
|
|
32
36
|
const onDown = (note, action) => (down) => {
|
|
33
37
|
if (down) {
|
|
34
|
-
if (note)
|
|
38
|
+
if (note) noteEvent("keyboard", "press", note);
|
|
35
39
|
action();
|
|
36
40
|
}
|
|
37
41
|
};
|
|
38
42
|
const alt = { alt: true, ctrl: false };
|
|
39
43
|
const ctrl = { alt: false, ctrl: true };
|
|
40
|
-
keyboard.registerKeyHandler(
|
|
41
|
-
keyboard.registerKeyHandler(
|
|
42
|
-
keyboard.registerKeyHandler(
|
|
43
|
-
keyboard.registerKeyHandler(
|
|
44
|
+
keyboard.registerKeyHandler(keyCodes.S, onDown("S", actions.enterDebugger), alt);
|
|
45
|
+
keyboard.registerKeyHandler(keyCodes.R, onDown(null, actions.reload), alt);
|
|
46
|
+
keyboard.registerKeyHandler(keyCodes.HOME, onDown("home", actions.enterDebugger), ctrl);
|
|
47
|
+
keyboard.registerKeyHandler(keyCodes.INSERT, onDown("insert", actions.toggleFast), ctrl);
|
|
44
48
|
keyboard.registerKeyHandler(
|
|
45
|
-
|
|
49
|
+
keyCodes.END,
|
|
46
50
|
onDown("end", () => keyboard.pauseEmulation()),
|
|
47
51
|
ctrl,
|
|
48
52
|
);
|
|
49
|
-
keyboard.registerKeyHandler(
|
|
50
|
-
keyboard.registerKeyHandler(
|
|
53
|
+
keyboard.registerKeyHandler(keyCodes.PAGEDOWN, onDown("pagedown", actions.openRewind), alt);
|
|
54
|
+
keyboard.registerKeyHandler(keyCodes.B, onDown(null, actions.openPrinter), ctrl);
|
|
51
55
|
|
|
52
56
|
// Alt+1-8 and Alt+F1-F8 trigger the accessibility switches. Using Alt means
|
|
53
57
|
// the underlying key is never forwarded to the BBC Micro (keyboard.js bails
|
|
@@ -55,8 +59,8 @@ export class KeyboardSetup {
|
|
|
55
59
|
// works normally.
|
|
56
60
|
const handleSwitch = (index) => (down) => accessibilitySwitches.setSwitch(index, down);
|
|
57
61
|
for (let i = 0; i < 8; i++) {
|
|
58
|
-
keyboard.registerKeyHandler(
|
|
59
|
-
keyboard.registerKeyHandler(
|
|
62
|
+
keyboard.registerKeyHandler(keyCodes.K1 + i, handleSwitch(i), alt);
|
|
63
|
+
keyboard.registerKeyHandler(keyCodes.F1 + i, handleSwitch(i), alt);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
document.addEventListener("keydown", (evt) => {
|
|
@@ -72,28 +76,4 @@ export class KeyboardSetup {
|
|
|
72
76
|
if (text) actions.paste(text);
|
|
73
77
|
});
|
|
74
78
|
}
|
|
75
|
-
|
|
76
|
-
sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
|
|
77
|
-
this.keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
clearKeys() {
|
|
81
|
-
this.keyboard.clearKeys();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
setKeyLayout(keyLayout) {
|
|
85
|
-
this.keyboard.setKeyLayout(keyLayout);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
resumeEmulation() {
|
|
89
|
-
this.keyboard.resumeEmulation();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
setRunning(running) {
|
|
93
|
-
this.keyboard.setRunning(running);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
postFrameShouldPause() {
|
|
97
|
-
return this.keyboard.postFrameShouldPause();
|
|
98
|
-
}
|
|
99
79
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import { ATOM } from "./utils_atom.js";
|
|
1
|
+
import { keyCodes } from "../keymap.js";
|
|
2
|
+
import { Typist } from "../typist.js";
|
|
4
3
|
|
|
5
4
|
const isMac = typeof window !== "undefined" && /^Mac/i.test(window.navigator?.platform || "");
|
|
6
5
|
|
|
@@ -29,13 +28,8 @@ export class Keyboard extends EventTarget {
|
|
|
29
28
|
this.inputEnabledFunction = inputEnabledFunction;
|
|
30
29
|
this.dbgr = dbgr;
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
31
|
+
this.keyInterface = processor.keyboardInterface;
|
|
32
|
+
this.typist = new Typist(processor);
|
|
39
33
|
|
|
40
34
|
// State
|
|
41
35
|
this.emuKeyHandlers = {};
|
|
@@ -49,13 +43,6 @@ export class Keyboard extends EventTarget {
|
|
|
49
43
|
this.lastShiftLocation = 1;
|
|
50
44
|
this.lastCtrlLocation = 1;
|
|
51
45
|
this.lastAltLocation = 1;
|
|
52
|
-
|
|
53
|
-
// Paste state — uses a scheduler task instead of a debugInstruction
|
|
54
|
-
// hook so the CPU can remain on the fast execution path during paste.
|
|
55
|
-
this._pasteKeys = [];
|
|
56
|
-
this._pasteLastChar = undefined;
|
|
57
|
-
this._pasteClocksPerMs = 0;
|
|
58
|
-
this._pasteTask = this.processor.scheduler.newTask(() => this._deliverPasteKey());
|
|
59
46
|
}
|
|
60
47
|
|
|
61
48
|
/**
|
|
@@ -65,7 +52,6 @@ export class Keyboard extends EventTarget {
|
|
|
65
52
|
*/
|
|
66
53
|
keyCode(evt) {
|
|
67
54
|
const ret = evt.which || evt.charCode || evt.keyCode;
|
|
68
|
-
const keyCodes = utils.keyCodes;
|
|
69
55
|
|
|
70
56
|
switch (evt.location) {
|
|
71
57
|
default:
|
|
@@ -108,9 +94,9 @@ export class Keyboard extends EventTarget {
|
|
|
108
94
|
case 3: // numpad
|
|
109
95
|
switch (ret) {
|
|
110
96
|
case keyCodes.ENTER:
|
|
111
|
-
return
|
|
97
|
+
return keyCodes.NUMPADENTER;
|
|
112
98
|
case keyCodes.DELETE:
|
|
113
|
-
return
|
|
99
|
+
return keyCodes.NUMPAD_DECIMAL_POINT;
|
|
114
100
|
}
|
|
115
101
|
break;
|
|
116
102
|
}
|
|
@@ -143,7 +129,7 @@ export class Keyboard extends EventTarget {
|
|
|
143
129
|
*/
|
|
144
130
|
setKeyLayout(layout) {
|
|
145
131
|
this.keyLayout = layout;
|
|
146
|
-
this.
|
|
132
|
+
this.processor.setKeyLayout(layout);
|
|
147
133
|
}
|
|
148
134
|
|
|
149
135
|
/**
|
|
@@ -226,7 +212,7 @@ export class Keyboard extends EventTarget {
|
|
|
226
212
|
const code = this.keyCode(evt);
|
|
227
213
|
evt.preventDefault();
|
|
228
214
|
|
|
229
|
-
if (this.isPasting && code ===
|
|
215
|
+
if (this.isPasting && code === keyCodes.ESCAPE) {
|
|
230
216
|
this.cancelPaste();
|
|
231
217
|
return;
|
|
232
218
|
}
|
|
@@ -255,11 +241,11 @@ export class Keyboard extends EventTarget {
|
|
|
255
241
|
* @private
|
|
256
242
|
*/
|
|
257
243
|
_handleSpecialKeys(code) {
|
|
258
|
-
if (code ===
|
|
244
|
+
if (code === keyCodes.F12 || code === keyCodes.BREAK) {
|
|
259
245
|
this.dispatchEvent(new CustomEvent("break", { detail: true }));
|
|
260
246
|
this.processor.setReset(true);
|
|
261
247
|
return true;
|
|
262
|
-
} else if (isMac && code ===
|
|
248
|
+
} else if (isMac && code === keyCodes.CAPSLOCK) {
|
|
263
249
|
// Special CapsLock handling for Mac
|
|
264
250
|
this.handleMacCapsLock();
|
|
265
251
|
return true;
|
|
@@ -286,11 +272,11 @@ export class Keyboard extends EventTarget {
|
|
|
286
272
|
evt.preventDefault();
|
|
287
273
|
|
|
288
274
|
// Handle special key cases
|
|
289
|
-
if (code ===
|
|
275
|
+
if (code === keyCodes.F12 || code === keyCodes.BREAK) {
|
|
290
276
|
this.dispatchEvent(new CustomEvent("break", { detail: false }));
|
|
291
277
|
this.processor.setReset(false);
|
|
292
278
|
return;
|
|
293
|
-
} else if (isMac && code ===
|
|
279
|
+
} else if (isMac && code === keyCodes.CAPSLOCK) {
|
|
294
280
|
// Special CapsLock handling for Mac
|
|
295
281
|
this.handleMacCapsLock();
|
|
296
282
|
return;
|
|
@@ -311,10 +297,10 @@ export class Keyboard extends EventTarget {
|
|
|
311
297
|
|
|
312
298
|
// Mac browsers seem to model caps lock as a physical key that's down when capslock is on, and up when it's off.
|
|
313
299
|
// No event is generated when it is physically released on the keyboard. So, we simulate a "tap" here.
|
|
314
|
-
this.keyInterface.keyDown(
|
|
300
|
+
this.keyInterface.keyDown(keyCodes.CAPSLOCK);
|
|
315
301
|
|
|
316
302
|
// Simulate a key release after a short delay
|
|
317
|
-
setTimeout(() => this.keyInterface.keyUp(
|
|
303
|
+
setTimeout(() => this.keyInterface.keyUp(keyCodes.CAPSLOCK), CAPS_LOCK_DELAY);
|
|
318
304
|
|
|
319
305
|
if (this.saidCapsLockIsTapped) return;
|
|
320
306
|
this.saidCapsLockIsTapped = true;
|
|
@@ -331,104 +317,17 @@ export class Keyboard extends EventTarget {
|
|
|
331
317
|
);
|
|
332
318
|
}
|
|
333
319
|
|
|
334
|
-
/**
|
|
335
|
-
* Send raw keyboard input to the emulated machine (for paste/autotype).
|
|
336
|
-
* @param {Array} keysToSend - Array of machine-specific key codes to send
|
|
337
|
-
* @param {boolean} checkCapsAndShiftLocks - Whether to check caps and shift locks
|
|
338
|
-
*/
|
|
320
|
+
/** Sends raw keys, and millisecond delays, to the machine: paste and autoboot come through here. */
|
|
339
321
|
sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
this.keyInterface.disableKeyboard();
|
|
343
|
-
// The paste task lives on the processor's scheduler, which is polled with peripheral
|
|
344
|
-
// cycles, so paste delays stay in real time whatever the CPU multiplier is.
|
|
345
|
-
this._pasteClocksPerMs = this.processor.peripheralCyclesPerSecond / 1000;
|
|
346
|
-
|
|
347
|
-
if (checkCapsAndShiftLocks) {
|
|
348
|
-
let toggleKey = null;
|
|
349
|
-
if (!this.keyInterface.capsLockLight) toggleKey = utils.BBC.CAPSLOCK;
|
|
350
|
-
else if (this.keyInterface.shiftLockLight) toggleKey = utils.BBC.SHIFTLOCK;
|
|
351
|
-
if (toggleKey) {
|
|
352
|
-
keysToSend.unshift(toggleKey);
|
|
353
|
-
keysToSend.push(toggleKey);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
this._pasteKeys = keysToSend;
|
|
358
|
-
this._pasteLastChar = undefined;
|
|
359
|
-
this._pasteTask.schedule(0);
|
|
322
|
+
this.typist.type(keysToSend, checkCapsAndShiftLocks);
|
|
360
323
|
}
|
|
361
324
|
|
|
362
|
-
/**
|
|
363
|
-
* Scheduler callback that delivers one key per invocation, rescheduling
|
|
364
|
-
* itself for the next key. Replaces the old debugInstruction hook so the
|
|
365
|
-
* CPU stays on the fast execution path during paste.
|
|
366
|
-
* @private
|
|
367
|
-
*/
|
|
368
|
-
_deliverPasteKey() {
|
|
369
|
-
if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey) {
|
|
370
|
-
this.keyInterface.keyToggleRaw(this._pasteLastChar);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (this._pasteKeys.length === 0) {
|
|
374
|
-
this._pasteLastChar = undefined;
|
|
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);
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const ch = this._pasteKeys[0];
|
|
389
|
-
const debounce = this._pasteLastChar === ch;
|
|
390
|
-
this._pasteLastChar = ch;
|
|
391
|
-
if (debounce) {
|
|
392
|
-
this._pasteLastChar = undefined;
|
|
393
|
-
this._pasteTask.schedule(30 * this._pasteClocksPerMs);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
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;
|
|
401
|
-
if (typeof this._pasteLastChar === "number") {
|
|
402
|
-
delayMs = this._pasteLastChar;
|
|
403
|
-
this._pasteLastChar = undefined;
|
|
404
|
-
} else {
|
|
405
|
-
this.keyInterface.keyToggleRaw(this._pasteLastChar);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
this._pasteKeys.shift();
|
|
409
|
-
this._pasteTask.schedule(delayMs * this._pasteClocksPerMs);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Cancel any in-progress paste operation.
|
|
414
|
-
*/
|
|
415
325
|
cancelPaste() {
|
|
416
|
-
|
|
417
|
-
this._pasteTask.cancel();
|
|
418
|
-
if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey) {
|
|
419
|
-
this.keyInterface.keyToggleRaw(this._pasteLastChar);
|
|
420
|
-
}
|
|
421
|
-
this._pasteLastChar = undefined;
|
|
422
|
-
this._pasteKeys = [];
|
|
423
|
-
this.keyInterface.enableKeyboard();
|
|
326
|
+
this.typist.cancel();
|
|
424
327
|
}
|
|
425
328
|
|
|
426
|
-
/**
|
|
427
|
-
* Whether a paste operation is currently in progress.
|
|
428
|
-
* @returns {boolean}
|
|
429
|
-
*/
|
|
430
329
|
get isPasting() {
|
|
431
|
-
return this.
|
|
330
|
+
return this.typist.isTyping;
|
|
432
331
|
}
|
|
433
332
|
|
|
434
333
|
/**
|
package/src/web/layout.js
CHANGED
|
@@ -78,6 +78,10 @@ export class Layout {
|
|
|
78
78
|
this.cubMonitorPic = document.getElementById("cub-monitor-pic");
|
|
79
79
|
this.borderReservedSize = embed ? 0 : 100;
|
|
80
80
|
this.bottomReservedSize = embed ? 0 : 68;
|
|
81
|
+
if (embed) {
|
|
82
|
+
for (const el of document.querySelectorAll(".embed-hide")) el.style.display = "none";
|
|
83
|
+
document.body.style.backgroundColor = "transparent";
|
|
84
|
+
}
|
|
81
85
|
|
|
82
86
|
window.addEventListener("resize", () => this.resize());
|
|
83
87
|
window.setTimeout(() => this.resize(), 1);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DiscLayout } from "../disc.js";
|
|
2
|
+
import { discFor, guessDiscTypeFromName } from "../fdc.js";
|
|
3
|
+
import { stringToUint8Array, uint8ArrayToString } from "../binary.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create or open a disc held in the browser's local storage.
|
|
7
|
+
* @param {string} name - The file name with extension
|
|
8
|
+
* @param {string} [layout] - one of DiscLayout; by default the image is asked what it is
|
|
9
|
+
* @param {function(*): void} [onSaveError] - called with whatever was thrown, the first time a write
|
|
10
|
+
* cannot be stored
|
|
11
|
+
* @returns {Disc} The loaded disc object
|
|
12
|
+
*/
|
|
13
|
+
export function localDisc(name, layout = DiscLayout.auto, onSaveError = () => {}) {
|
|
14
|
+
const discName = "disc_" + name;
|
|
15
|
+
let data;
|
|
16
|
+
const dataString = window.localStorage[discName];
|
|
17
|
+
if (!dataString) {
|
|
18
|
+
console.log("Creating browser-local disc " + name);
|
|
19
|
+
const discType = guessDiscTypeFromName(name);
|
|
20
|
+
if (!discType.byteSize) {
|
|
21
|
+
throw new Error(`Cannot create blank disc of type ${discType.extension} - unknown size`);
|
|
22
|
+
}
|
|
23
|
+
data = new Uint8Array(discType.byteSize);
|
|
24
|
+
if (discType.supportsCatalogue) {
|
|
25
|
+
discType.setDiscName(data, name);
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
console.log("Loading browser-local disc " + name);
|
|
29
|
+
data = stringToUint8Array(dataString);
|
|
30
|
+
}
|
|
31
|
+
let reportedSaveError = false;
|
|
32
|
+
const onChange = (data) => {
|
|
33
|
+
try {
|
|
34
|
+
const str = uint8ArrayToString(data);
|
|
35
|
+
window.localStorage.setItem(discName, str);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log(`Unable to save browser-local disc ${name}: ${e}`);
|
|
38
|
+
if (reportedSaveError) return;
|
|
39
|
+
reportedSaveError = true;
|
|
40
|
+
onSaveError(e);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return discFor(name, data, onChange, layout);
|
|
44
|
+
}
|
package/src/web/machine.js
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { machineSpec } from "../machine-spec.js";
|
|
2
2
|
import { Cmos, localStoragePersistence } from "../cmos.js";
|
|
3
3
|
import { Econet } from "../econet.js";
|
|
4
4
|
import { LoadSD } from "../mmc.js";
|
|
5
5
|
import { tubeModelFor } from "../models.js";
|
|
6
|
-
import * as utils from "../utils.js";
|
|
7
6
|
import { toast } from "./toast.js";
|
|
8
7
|
import { errorText, reportLoadFailure, showNotice } from "./reporting.js";
|
|
8
|
+
import { loadData } from "../loader.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* The machine's fittings as the CPU wants them handed over. Pure, so the bank
|
|
12
12
|
* and flag decisions are testable on their own.
|
|
13
13
|
*/
|
|
14
|
-
export function buildEmulationConfig({
|
|
15
|
-
|
|
14
|
+
export function buildEmulationConfig({
|
|
15
|
+
settings,
|
|
16
|
+
parsedQuery,
|
|
17
|
+
keyLayout,
|
|
18
|
+
cpuMultiplier,
|
|
19
|
+
extraRoms,
|
|
20
|
+
userPort,
|
|
21
|
+
printer,
|
|
22
|
+
}) {
|
|
23
|
+
return machineSpec({
|
|
16
24
|
keyLayout,
|
|
17
25
|
cpuMultiplier,
|
|
18
|
-
tubeCpuMultiplier:
|
|
26
|
+
tubeCpuMultiplier: settings.tubeCpuMultiplier,
|
|
19
27
|
videoCyclesBatch: parsedQuery.videoCyclesBatch,
|
|
20
|
-
tube:
|
|
21
|
-
hasMusic5000:
|
|
22
|
-
hasTeletextAdaptor:
|
|
28
|
+
tube: settings.coProcessor ? tubeModelFor(settings.model) : null,
|
|
29
|
+
hasMusic5000: settings.hasMusic5000,
|
|
30
|
+
hasTeletextAdaptor: settings.hasTeletextAdaptor,
|
|
23
31
|
// ROM order determines sideways bank allocation, and the fittings' ROMs claim banks
|
|
24
32
|
// before any the user asked for with ?rom=.
|
|
25
|
-
extraRoms: [...
|
|
33
|
+
extraRoms: [...settings.extraRoms, ...extraRoms],
|
|
26
34
|
userPort,
|
|
27
35
|
printerPort: printer,
|
|
28
36
|
getGamepads: function () {
|
|
@@ -33,14 +41,14 @@ export function buildEmulationConfig({ config, parsedQuery, keyLayout, cpuMultip
|
|
|
33
41
|
logFdcCommands: parsedQuery.logFdcCommands !== undefined,
|
|
34
42
|
logFdcStateChanges: parsedQuery.logFdcStateChanges !== undefined,
|
|
35
43
|
},
|
|
36
|
-
};
|
|
44
|
+
});
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
/** The emulated machine itself: the processor with everything bolted to it, and its start-up. */
|
|
40
48
|
export class Machine {
|
|
41
49
|
constructor({
|
|
42
50
|
model,
|
|
43
|
-
|
|
51
|
+
settings,
|
|
44
52
|
parsedQuery,
|
|
45
53
|
keyLayout,
|
|
46
54
|
cpuMultiplier,
|
|
@@ -52,14 +60,14 @@ export class Machine {
|
|
|
52
60
|
video,
|
|
53
61
|
audioHandler,
|
|
54
62
|
dbgr,
|
|
55
|
-
|
|
63
|
+
build = (cpuModel, options) => new cpuModel.Cpu(cpuModel, options),
|
|
56
64
|
}) {
|
|
57
65
|
this.model = model;
|
|
58
66
|
this.audioHandler = audioHandler;
|
|
59
67
|
this.speechOutput = speechOutput;
|
|
60
68
|
|
|
61
69
|
this.econet = null;
|
|
62
|
-
if (
|
|
70
|
+
if (settings.hasEconet) {
|
|
63
71
|
this.econet = new Econet(stationId, model.cyclesPerSecond);
|
|
64
72
|
} else {
|
|
65
73
|
document.getElementById("fsmenuitem").style.display = "none";
|
|
@@ -78,8 +86,8 @@ export class Machine {
|
|
|
78
86
|
this.econet,
|
|
79
87
|
);
|
|
80
88
|
|
|
81
|
-
this.
|
|
82
|
-
|
|
89
|
+
this.spec = buildEmulationConfig({
|
|
90
|
+
settings,
|
|
83
91
|
parsedQuery,
|
|
84
92
|
keyLayout,
|
|
85
93
|
cpuMultiplier,
|
|
@@ -88,17 +96,16 @@ export class Machine {
|
|
|
88
96
|
printer,
|
|
89
97
|
});
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
this.processor = makeCpu(CpuClass, model, {
|
|
99
|
+
this.processor = build(model, {
|
|
93
100
|
dbgr,
|
|
94
101
|
video,
|
|
95
102
|
soundChip: audioHandler.soundChip,
|
|
96
103
|
ddNoise: audioHandler.ddNoise,
|
|
97
104
|
relayNoise: audioHandler.relayNoise,
|
|
98
|
-
music5000:
|
|
105
|
+
music5000: settings.hasMusic5000 ? audioHandler.music5000 : null,
|
|
99
106
|
cmos: this.cmos,
|
|
100
|
-
config: this.emulationConfig,
|
|
101
107
|
econet: this.econet,
|
|
108
|
+
config: this.spec,
|
|
102
109
|
});
|
|
103
110
|
|
|
104
111
|
printer.attach(this.processor.uservia);
|
|
@@ -187,7 +194,7 @@ export class Machine {
|
|
|
187
194
|
await startImageLoad(`BASIC program ${loadBasic}`, () =>
|
|
188
195
|
autoBoot.insertBasic(
|
|
189
196
|
(async () => {
|
|
190
|
-
const data = await
|
|
197
|
+
const data = await loadData(loadBasic);
|
|
191
198
|
return String.fromCharCode.apply(null, data);
|
|
192
199
|
})(),
|
|
193
200
|
basicNeedsRun,
|