jsbeeb 1.22.0 → 1.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ import * as utils from "../utils.js";
2
+ import * as bootstrap from "bootstrap";
3
+ import { BbcDiscArchive, Provenance, describe as describeHfe, matches, provenancesIn } from "../bbcdiscs.js";
4
+ import { errorText } from "./reporting.js";
5
+ import { clearArchiveList, showArchiveMessage } from "./archive-list.js";
6
+
7
+ const HfeProvenanceLabels = {
8
+ [Provenance.Captured]: ["Captured", "Direct from disc"],
9
+ [Provenance.Reconstructed]: ["Reconstructed", "Rebuilt from a sector dump"],
10
+ };
11
+
12
+ const showHfeRow = (row, file, filter, shown) => (row.style.display = matches(file, filter, shown) ? "" : "none");
13
+
14
+ /**
15
+ * The HFE archive picker: the catalogue with its filter and provenance
16
+ * choices, rendered a batch at a time under a ticket so a list that has been
17
+ * emptied is not appended to by a stale chain.
18
+ */
19
+ export class HfePicker {
20
+ constructor({ media, drives, modals, urlState, processor, autoboot }) {
21
+ this.media = media;
22
+ this.drives = drives;
23
+ this.modals = modals;
24
+ this.urlState = urlState;
25
+ this.processor = processor;
26
+ this.autoboot = autoboot;
27
+
28
+ // Rendering is spread over several turns of the event loop, so a list that has
29
+ // been emptied may still have a chain of appends heading for it. Anything that
30
+ // clears the list takes a new ticket; a chain whose ticket is stale gives up.
31
+ this.renderTicket = 0;
32
+
33
+ this.archive = new BbcDiscArchive(
34
+ () => {
35
+ this.renderTicket++;
36
+ showArchiveMessage("hfe", "hfe-list", "Loading catalogue from HFE archive");
37
+ },
38
+ (catalogue) => this.renderCatalogue(catalogue),
39
+ () => {
40
+ this.renderTicket++;
41
+ showArchiveMessage("hfe", "hfe-list", "There was an error accessing the HFE archive");
42
+ },
43
+ );
44
+
45
+ this.modal = new bootstrap.Modal(document.getElementById("hfe"));
46
+ document.getElementById("hfe").addEventListener("shown.bs.modal", () => {
47
+ document.getElementById("hfe-filter").focus();
48
+ });
49
+ document.getElementById("hfe").addEventListener("show.bs.modal", () => this.archive.populate());
50
+
51
+ this.filter = document.getElementById("hfe-filter");
52
+ this.provenance = document.getElementById("hfe-provenance");
53
+ const onFilter = () => this.applyFilter();
54
+ this.filter.addEventListener("change", onFilter);
55
+ this.filter.addEventListener("keyup", onFilter);
56
+ }
57
+
58
+ async pick(file) {
59
+ utils.noteEvent("hfe", "click", file.path);
60
+ this.media.setDisc1Image("hfe:" + file.path);
61
+ const needsAutoboot = this.urlState.params.autoboot !== undefined;
62
+ if (needsAutoboot) this.processor.reset(true);
63
+
64
+ const name = describeHfe(file).title;
65
+ this.modals.popupLoading("Loading " + name);
66
+ try {
67
+ const loaded = await this.media.loadDiscImage(this.urlState.params.disc1, this.drives.layoutForDrive(0));
68
+ this.drives.putDiscIn(0, loaded);
69
+ this.modals.loadingFinished();
70
+ if (needsAutoboot) this.autoboot(name);
71
+ } catch (err) {
72
+ console.error("Error loading disc image:", err);
73
+ this.modals.loadingFinished(`Unable to load ${name} from the HFE archive: ${errorText(err)}`);
74
+ }
75
+ }
76
+
77
+ renderCatalogue(catalogue) {
78
+ const ticket = ++this.renderTicket;
79
+ clearArchiveList("hfe-list");
80
+ const list = document.getElementById("hfe-list");
81
+ document.querySelector("#hfe .loading").style.display = "none";
82
+ const template = list.querySelector(".template");
83
+ this.showProvenanceChoices(catalogue);
84
+
85
+ const addSome = (remaining) => {
86
+ if (ticket !== this.renderTicket) return;
87
+ const MaxAtATime = 100;
88
+ const Delay = 30;
89
+ // Read per batch: both can be changed while this is still going.
90
+ const filter = this.filter.value.toLowerCase();
91
+ const shown = this.shownProvenances();
92
+ for (const file of remaining.slice(0, MaxAtATime)) {
93
+ const { title, publisher, detail } = describeHfe(file);
94
+ const row = template.cloneNode(true);
95
+ row.classList.remove("template");
96
+ row.querySelector(".name").textContent = title;
97
+ row.querySelector(".publisher").textContent = publisher;
98
+ row.querySelector(".detail").textContent = detail;
99
+ row.querySelector(".provenance").textContent =
100
+ file.provenance === Provenance.Reconstructed ? "reconstructed" : "";
101
+ if (file.notes) row.title = file.notes;
102
+ // The row is an anchor, and letting it navigate to "#" would push a
103
+ // history entry of its own on top of the one updateUrl pushes.
104
+ row.addEventListener("click", (event) => {
105
+ event.preventDefault();
106
+ this.pick(file);
107
+ this.modal.hide();
108
+ });
109
+ row.hfeFile = file;
110
+ list.appendChild(row);
111
+ showHfeRow(row, file, filter, shown);
112
+ }
113
+ if (remaining.length > MaxAtATime) setTimeout(() => addSome(remaining.slice(MaxAtATime)), Delay);
114
+ };
115
+ addSome(catalogue);
116
+ }
117
+
118
+ /** Which provenances the picker is showing, or null when it is not offering the choice. */
119
+ shownProvenances() {
120
+ const boxes = [...this.provenance.querySelectorAll("input")];
121
+ return boxes.length ? new Set(boxes.filter((box) => box.checked).map((box) => box.value)) : null;
122
+ }
123
+
124
+ // Offer one tick per provenance the archive actually holds, rather than naming
125
+ // them here: a source added later should appear without this having to change.
126
+ showProvenanceChoices(catalogue) {
127
+ const present = provenancesIn(catalogue);
128
+ // Nothing to choose between: no ticks, and shownProvenances says "all".
129
+ if (present.length < 2) {
130
+ this.provenance.replaceChildren();
131
+ return;
132
+ }
133
+ const wasShown = this.shownProvenances();
134
+ this.provenance.replaceChildren(
135
+ ...present.map((provenance) => {
136
+ const [text, why] = HfeProvenanceLabels[provenance] ?? [provenance, ""];
137
+ const label = document.createElement("label");
138
+ label.title = why;
139
+ const box = document.createElement("input");
140
+ box.type = "checkbox";
141
+ box.value = provenance;
142
+ box.checked = !wasShown || wasShown.has(provenance);
143
+ box.addEventListener("change", () => this.applyFilter());
144
+ label.append(box, text);
145
+ return label;
146
+ }),
147
+ );
148
+ }
149
+
150
+ applyFilter() {
151
+ const filter = this.filter.value.toLowerCase();
152
+ const shown = this.shownProvenances();
153
+ for (const row of document.querySelectorAll("#hfe-list li:not(.template)"))
154
+ showHfeRow(row, row.hfeFile, filter, shown);
155
+ }
156
+ }
@@ -0,0 +1,125 @@
1
+ import * as utils from "../utils.js";
2
+ import { Keyboard } from "../keyboard.js";
3
+ import { showNotice } from "./reporting.js";
4
+
5
+ /**
6
+ * The page's keyboard: the emulated one, the browser shortcuts around it, and
7
+ * the accessibility switches on the user port. Built in two steps because the
8
+ * user port has to exist before the processor does, and the keyboard after.
9
+ */
10
+ export class KeyboardSetup {
11
+ /**
12
+ * @param {object} actions what each shortcut does, supplied late-bound:
13
+ * enterDebugger, reload, toggleFast, openRewind, openPrinter,
14
+ * pause, resume, onAnyKeyDown
15
+ */
16
+ constructor(actions) {
17
+ this.actions = actions;
18
+ this.keyboard = null;
19
+
20
+ // Accessibility switch state: bits 0-7 correspond to switches 1-8.
21
+ // Active low: 0xff = no switches pressed; clearing a bit = that switch is pressed.
22
+ this.switchState = 0xff;
23
+ const setup = this;
24
+ this.userPort = {
25
+ write() {},
26
+ read() {
27
+ return setup.switchState;
28
+ },
29
+ };
30
+ }
31
+
32
+ /** Initialise keyboard now that processor exists */
33
+ attach({ processor, dbgr, keyLayout }) {
34
+ const { actions } = this;
35
+ const keyboard = (this.keyboard = new Keyboard({
36
+ processor,
37
+ inputEnabledFunction: () => document.activeElement && document.activeElement.id === "paste-text",
38
+ keyLayout,
39
+ dbgr,
40
+ }));
41
+ keyboard.addEventListener("notice", showNotice);
42
+ keyboard.addEventListener("pause", () => actions.pause());
43
+ keyboard.addEventListener("resume", () => actions.resume());
44
+ keyboard.addEventListener("break", (e) => {
45
+ // F12/Break: Reset processor
46
+ if (e.detail) utils.noteEvent("keyboard", "press", "break");
47
+ });
48
+
49
+ const onDown = (note, action) => (down) => {
50
+ if (down) {
51
+ if (note) utils.noteEvent("keyboard", "press", note);
52
+ action();
53
+ }
54
+ };
55
+ const alt = { alt: true, ctrl: false };
56
+ const ctrl = { alt: false, ctrl: true };
57
+ keyboard.registerKeyHandler(utils.keyCodes.S, onDown("S", actions.enterDebugger), alt);
58
+ keyboard.registerKeyHandler(utils.keyCodes.R, onDown(null, actions.reload), alt);
59
+ keyboard.registerKeyHandler(utils.keyCodes.HOME, onDown("home", actions.enterDebugger), ctrl);
60
+ keyboard.registerKeyHandler(utils.keyCodes.INSERT, onDown("insert", actions.toggleFast), ctrl);
61
+ keyboard.registerKeyHandler(
62
+ utils.keyCodes.END,
63
+ onDown("end", () => keyboard.pauseEmulation()),
64
+ ctrl,
65
+ );
66
+ keyboard.registerKeyHandler(utils.keyCodes.PAGEDOWN, onDown("pagedown", actions.openRewind), alt);
67
+ keyboard.registerKeyHandler(utils.keyCodes.B, onDown(null, actions.openPrinter), ctrl);
68
+
69
+ // Register accessibility switch key handlers.
70
+ // Keys 1-8 (K1-K8) and function keys F1-F8 both map to user port bits 0-7
71
+ // (active low: pressing the key clears the corresponding bit in &FE60).
72
+ //
73
+ // On real hardware, the Brilliant Computing switch interface box and special-ed
74
+ // joystick connect to the User Port only; they do not touch the analogue port
75
+ // or the System VIA fire buttons (PB4/PB5), which belong to the standard
76
+ // analogue joystick connector. So we only update switchState here.
77
+ const handleSwitch = (bit) => (down) => {
78
+ if (down) this.switchState &= ~(1 << bit);
79
+ else this.switchState |= 1 << bit;
80
+ };
81
+
82
+ // Alt+1-8 and Alt+F1-F8 trigger the switches. Using Alt means the underlying
83
+ // key is never forwarded to the BBC Micro (keyboard.js bails out early when a
84
+ // handler fires), so typing numbers or using function keys works normally.
85
+ for (let i = 0; i < 8; i++) {
86
+ keyboard.registerKeyHandler(utils.keyCodes.K1 + i, handleSwitch(i), alt);
87
+ keyboard.registerKeyHandler(utils.keyCodes.F1 + i, handleSwitch(i), alt);
88
+ }
89
+
90
+ document.addEventListener("keydown", (evt) => {
91
+ actions.onAnyKeyDown();
92
+ keyboard.keyDown(evt);
93
+ });
94
+ document.addEventListener("keypress", (evt) => keyboard.keyPress(evt));
95
+ document.addEventListener("keyup", (evt) => keyboard.keyUp(evt));
96
+ }
97
+
98
+ sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
99
+ if (this.keyboard) {
100
+ this.keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
101
+ } else {
102
+ console.warn("Tried to send keys before keyboard was initialised");
103
+ }
104
+ }
105
+
106
+ clearKeys() {
107
+ this.keyboard.clearKeys();
108
+ }
109
+
110
+ setKeyLayout(keyLayout) {
111
+ this.keyboard.setKeyLayout(keyLayout);
112
+ }
113
+
114
+ resumeEmulation() {
115
+ this.keyboard.resumeEmulation();
116
+ }
117
+
118
+ setRunning(running) {
119
+ this.keyboard.setRunning(running);
120
+ }
121
+
122
+ postFrameShouldPause() {
123
+ return this.keyboard.postFrameShouldPause();
124
+ }
125
+ }
@@ -0,0 +1,156 @@
1
+ import { toast } from "./toast.js";
2
+ import { errorText } from "./reporting.js";
3
+
4
+ /** Steps the drawing buffer grows in, as a multiple of the base canvas size. */
5
+ const CanvasScaleStep = 0.25;
6
+
7
+ /**
8
+ * Where everything goes for a given window: the monitor picture, the canvas
9
+ * within it, and (for a mode with maxCanvasScale) how large a drawing buffer
10
+ * to ask for. Pure, so the geometry is testable on its own.
11
+ */
12
+ export function fitMonitor(displayConfig, viewport, canvasNative) {
13
+ const imageOrigHeight = displayConfig.imageHeight;
14
+ const imageOrigWidth = displayConfig.imageWidth;
15
+ const desiredAspectRatio = imageOrigWidth / imageOrigHeight;
16
+ const minWidth = imageOrigWidth / 4;
17
+ const minHeight = imageOrigHeight / 4;
18
+
19
+ let width = Math.max(minWidth, viewport.innerWidth - viewport.borderReservedSize * 2);
20
+ let height = Math.max(minHeight, viewport.innerHeight - viewport.navbarHeight - viewport.bottomReservedSize);
21
+ if (width / height <= desiredAspectRatio) {
22
+ height = width / desiredAspectRatio;
23
+ } else {
24
+ width = height * desiredAspectRatio;
25
+ }
26
+
27
+ const containerScale = width / imageOrigWidth;
28
+ const scaledVisibleWidth = displayConfig.visibleWidth * containerScale;
29
+ const scaledVisibleHeight = displayConfig.visibleHeight * containerScale;
30
+
31
+ const canvasAspect = canvasNative.width / canvasNative.height;
32
+ const visibleAspect = scaledVisibleWidth / scaledVisibleHeight;
33
+
34
+ let finalCanvasWidth, finalCanvasHeight;
35
+ if (canvasAspect > visibleAspect) {
36
+ finalCanvasWidth = scaledVisibleWidth;
37
+ finalCanvasHeight = scaledVisibleWidth / canvasAspect;
38
+ } else {
39
+ finalCanvasHeight = scaledVisibleHeight;
40
+ finalCanvasWidth = scaledVisibleHeight * canvasAspect;
41
+ }
42
+
43
+ // A mode that reconstructs detail wants to draw at the size it will be
44
+ // seen at, up to the limit it asks for. Drawing more than the display
45
+ // can show costs fragments and buys nothing, and for an expensive
46
+ // shader that is the difference between comfortable and not.
47
+ let backing = null;
48
+ if (displayConfig.maxCanvasScale) {
49
+ const wanted = (finalCanvasWidth * viewport.devicePixelRatio) / displayConfig.canvasWidth;
50
+ // Quantised, because resize fires continuously while a window is
51
+ // dragged and every distinct value reallocates the drawing buffer.
52
+ const quantised = Math.round(wanted / CanvasScaleStep) * CanvasScaleStep;
53
+ const scale = Math.min(displayConfig.maxCanvasScale, Math.max(1, quantised));
54
+ backing = {
55
+ width: Math.round(displayConfig.canvasWidth * scale),
56
+ height: Math.round(displayConfig.canvasHeight * scale),
57
+ };
58
+ }
59
+
60
+ return {
61
+ monitor: { width, height },
62
+ canvas: {
63
+ width: finalCanvasWidth,
64
+ height: finalCanvasHeight,
65
+ left: displayConfig.canvasLeft * containerScale,
66
+ top: displayConfig.canvasTop * containerScale,
67
+ },
68
+ backing,
69
+ };
70
+ }
71
+
72
+ /** Keeps the monitor and canvas fitted to the window, and wires the page furniture around them. */
73
+ export class Layout {
74
+ constructor({ screenCanvas, display, embed, sidebars = {} }) {
75
+ this.screenCanvas = screenCanvas;
76
+ this.display = display;
77
+ this.cubMonitor = document.getElementById("cub-monitor");
78
+ this.cubMonitorPic = document.getElementById("cub-monitor-pic");
79
+ this.borderReservedSize = embed ? 0 : 100;
80
+ this.bottomReservedSize = embed ? 0 : 68;
81
+
82
+ window.addEventListener("resize", () => this.resize());
83
+ window.setTimeout(() => this.resize(), 1);
84
+ window.setTimeout(() => this.resize(), 500);
85
+
86
+ this.bindSidebar(".sidebar.left", sidebars.left, (div, img) => {
87
+ div.style.left = -img.naturalWidth - 5 + "px";
88
+ });
89
+ this.bindSidebar(".sidebar.right", sidebars.right, (div, img) => {
90
+ div.style.right = -img.naturalWidth - 5 + "px";
91
+ });
92
+ this.bindSidebar(".sidebar.bottom", sidebars.bottom, (div, img) => {
93
+ div.style.bottom = -img.naturalHeight + "px";
94
+ });
95
+
96
+ const fullscreenItem = document.getElementById("fs");
97
+ if (document.fullscreenEnabled) {
98
+ fullscreenItem.addEventListener("click", async (event) => {
99
+ event.preventDefault();
100
+ try {
101
+ await screenCanvas.requestFullscreen();
102
+ } catch (error) {
103
+ toast(`Could not go fullscreen: ${errorText(error)}`, { title: "Fullscreen" });
104
+ }
105
+ });
106
+ } else {
107
+ fullscreenItem.closest("li").hidden = true;
108
+ }
109
+ }
110
+
111
+ resize() {
112
+ // The display config can change when the display mode switches.
113
+ const displayConfig = this.display.filterClass.getDisplayConfig();
114
+ const fitted = fitMonitor(
115
+ displayConfig,
116
+ {
117
+ innerWidth: window.innerWidth,
118
+ innerHeight: window.innerHeight,
119
+ navbarHeight: document.getElementById("header-bar")?.offsetHeight || 0,
120
+ borderReservedSize: this.borderReservedSize,
121
+ bottomReservedSize: this.bottomReservedSize,
122
+ devicePixelRatio: window.devicePixelRatio || 1,
123
+ },
124
+ { width: this.screenCanvas.getAttribute("width"), height: this.screenCanvas.getAttribute("height") },
125
+ );
126
+
127
+ this.cubMonitor.style.height = fitted.monitor.height + "px";
128
+ this.cubMonitor.style.width = fitted.monitor.width + "px";
129
+ this.cubMonitorPic.style.height = fitted.monitor.height + "px";
130
+ this.cubMonitorPic.style.width = fitted.monitor.width + "px";
131
+
132
+ if (fitted.backing && this.screenCanvas.width !== fitted.backing.width) {
133
+ this.screenCanvas.width = fitted.backing.width;
134
+ this.screenCanvas.height = fitted.backing.height;
135
+ // Resizing threw the drawing buffer away.
136
+ this.display.video.paint();
137
+ }
138
+
139
+ this.screenCanvas.style.width = fitted.canvas.width + "px";
140
+ this.screenCanvas.style.height = fitted.canvas.height + "px";
141
+ this.screenCanvas.style.left = fitted.canvas.left + "px";
142
+ this.screenCanvas.style.top = fitted.canvas.top + "px";
143
+ }
144
+
145
+ bindSidebar(selector, url, onload) {
146
+ const div = document.querySelector(selector);
147
+ const img = div.querySelector("img");
148
+ img.style.display = "none";
149
+ if (!url) return;
150
+ img.addEventListener("load", () => {
151
+ onload(div, img);
152
+ img.style.display = "";
153
+ });
154
+ img.src = url;
155
+ }
156
+ }
@@ -0,0 +1,204 @@
1
+ import { AtomCpu6502, Cpu6502 } from "../6502.js";
2
+ import { Cmos, localStoragePersistence } from "../cmos.js";
3
+ import { Econet } from "../econet.js";
4
+ import { LoadSD } from "../mmc.js";
5
+ import { tubeModelFor } from "../models.js";
6
+ import * as utils from "../utils.js";
7
+ import { toast } from "./toast.js";
8
+ import { errorText, reportLoadFailure, showNotice } from "./reporting.js";
9
+
10
+ /**
11
+ * The machine's fittings as the CPU wants them handed over. Pure, so the bank
12
+ * and flag decisions are testable on their own.
13
+ */
14
+ export function buildEmulationConfig({ config, parsedQuery, keyLayout, cpuMultiplier, extraRoms, userPort, printer }) {
15
+ return {
16
+ keyLayout,
17
+ cpuMultiplier,
18
+ tubeCpuMultiplier: config.tubeCpuMultiplier,
19
+ videoCyclesBatch: parsedQuery.videoCyclesBatch,
20
+ tube: config.coProcessor ? tubeModelFor(config.model) : null,
21
+ hasMusic5000: config.hasMusic5000,
22
+ hasTeletextAdaptor: config.hasTeletextAdaptor,
23
+ // ROM order determines sideways bank allocation, and the fittings' ROMs claim banks
24
+ // before any the user asked for with ?rom=.
25
+ extraRoms: [...config.extraRoms, ...extraRoms],
26
+ userPort,
27
+ printerPort: printer,
28
+ getGamepads: function () {
29
+ // Gamepads are only available in secure contexts. If e.g. loading from http:// urls they aren't there.
30
+ return navigator.getGamepads ? navigator.getGamepads() : [];
31
+ },
32
+ debugFlags: {
33
+ logFdcCommands: parsedQuery.logFdcCommands !== undefined,
34
+ logFdcStateChanges: parsedQuery.logFdcStateChanges !== undefined,
35
+ },
36
+ };
37
+ }
38
+
39
+ /** The emulated machine itself: the processor with everything bolted to it, and its start-up. */
40
+ export class Machine {
41
+ constructor({
42
+ model,
43
+ config,
44
+ parsedQuery,
45
+ keyLayout,
46
+ cpuMultiplier,
47
+ extraRoms,
48
+ stationId,
49
+ userPort,
50
+ printer,
51
+ speechOutput,
52
+ video,
53
+ audioHandler,
54
+ dbgr,
55
+ makeCpu = (CpuClass, ...args) => new CpuClass(...args),
56
+ }) {
57
+ this.model = model;
58
+ this.audioHandler = audioHandler;
59
+ this.speechOutput = speechOutput;
60
+
61
+ this.econet = null;
62
+ if (config.hasEconet) {
63
+ this.econet = new Econet(stationId, model.cyclesPerSecond);
64
+ } else {
65
+ document.getElementById("fsmenuitem").style.display = "none";
66
+ }
67
+
68
+ this.cmos = new Cmos(
69
+ localStoragePersistence(
70
+ () => window.localStorage,
71
+ (error) =>
72
+ toast(
73
+ `Settings changed with *CONFIGURE will not be kept (${errorText(error)}). Check that this site is allowed to store data, and that its storage is not full.`,
74
+ { title: "Settings", quietKey: "quietCmosSave" },
75
+ ),
76
+ ),
77
+ model.cmosOverride,
78
+ this.econet,
79
+ );
80
+
81
+ this.emulationConfig = buildEmulationConfig({
82
+ config,
83
+ parsedQuery,
84
+ keyLayout,
85
+ cpuMultiplier,
86
+ extraRoms,
87
+ userPort,
88
+ printer,
89
+ });
90
+
91
+ const CpuClass = model.isAtom ? AtomCpu6502 : Cpu6502;
92
+ this.processor = makeCpu(CpuClass, model, {
93
+ dbgr,
94
+ video,
95
+ soundChip: audioHandler.soundChip,
96
+ ddNoise: audioHandler.ddNoise,
97
+ relayNoise: audioHandler.relayNoise,
98
+ music5000: config.hasMusic5000 ? audioHandler.music5000 : null,
99
+ cmos: this.cmos,
100
+ config: this.emulationConfig,
101
+ econet: this.econet,
102
+ });
103
+
104
+ printer.attach(this.processor.uservia);
105
+ this.processor.teletextAdaptor?.addEventListener("notice", showNotice);
106
+ this.processor.acia.addEventListener("notice", showNotice);
107
+ }
108
+
109
+ /**
110
+ * Attach an RS-423 composite handler to the ACIA that combines the touchscreen
111
+ * (which sends position data to the BBC) with the speech output (which speaks
112
+ * text the BBC sends out).
113
+ */
114
+ setupRs423Handler() {
115
+ const { processor, speechOutput } = this;
116
+ processor.acia.setRs423Handler({
117
+ onTransmit(val) {
118
+ processor.touchScreen.onTransmit(val);
119
+ speechOutput.onTransmit(val);
120
+ },
121
+ tryReceive(rts) {
122
+ return processor.touchScreen.tryReceive(rts);
123
+ },
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Initialises the machine and starts every image the URL asked for, each
129
+ * reporting its own failure without stopping the boot (#808).
130
+ */
131
+ async start({
132
+ media,
133
+ drives,
134
+ autoBoot,
135
+ discImage,
136
+ secondDiscImage,
137
+ tape,
138
+ mmcImage,
139
+ loadBasic,
140
+ embedBasic,
141
+ basicNeedsRun,
142
+ }) {
143
+ const { processor } = this;
144
+ await Promise.all([this.audioHandler.initialise(), processor.initialise()]);
145
+
146
+ // Wire up the composite RS-423 handler now that the touchscreen exists.
147
+ this.setupRs423Handler();
148
+
149
+ // Ideally would start the loads first. But their completion needs the FDC from the processor
150
+ const imageLoads = [];
151
+
152
+ function startImageLoad(description, load) {
153
+ const loading = (async () => {
154
+ try {
155
+ await load();
156
+ } catch (error) {
157
+ reportLoadFailure(description, error);
158
+ }
159
+ })();
160
+ imageLoads.push(loading);
161
+ return loading;
162
+ }
163
+
164
+ if (discImage) {
165
+ startImageLoad(`disc ${discImage}`, async () =>
166
+ drives.putDiscIn(0, await media.loadDiscImage(discImage, drives.layoutForDrive(0))),
167
+ );
168
+ }
169
+
170
+ if (secondDiscImage) {
171
+ startImageLoad(`disc ${secondDiscImage}`, async () =>
172
+ drives.putDiscIn(1, await media.loadDiscImage(secondDiscImage, drives.layoutForDrive(1))),
173
+ );
174
+ }
175
+
176
+ if (tape) {
177
+ startImageLoad(`tape ${tape}`, async () => media.setProcessorTape(await media.loadTapeImage(tape)));
178
+ }
179
+
180
+ if (mmcImage && this.model.isAtom) {
181
+ startImageLoad(`MMC image ${mmcImage}`, async () => processor.atommc.SetMMCData(await LoadSD(mmcImage)));
182
+ }
183
+
184
+ if (loadBasic) {
185
+ await startImageLoad(`BASIC program ${loadBasic}`, () =>
186
+ autoBoot.insertBasic(
187
+ (async () => {
188
+ const data = await utils.loadData(loadBasic);
189
+ return String.fromCharCode.apply(null, data);
190
+ })(),
191
+ basicNeedsRun,
192
+ ),
193
+ );
194
+ }
195
+
196
+ if (embedBasic) {
197
+ await startImageLoad("the BASIC program from the URL", () =>
198
+ autoBoot.insertBasic(Promise.resolve(embedBasic), true),
199
+ );
200
+ }
201
+
202
+ return Promise.all(imageLoads);
203
+ }
204
+ }