jsbeeb 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/models.js CHANGED
@@ -263,6 +263,8 @@ export function findModel(name) {
263
263
  return null;
264
264
  }
265
265
 
266
+ export const DefaultModel = findModel("B-DFS1.2");
267
+
266
268
  export const TEST_6502 = new Model({
267
269
  name: "TEST",
268
270
  synonyms: ["TEST"],
package/src/sth.js CHANGED
@@ -50,12 +50,12 @@ export class StairwayToHell {
50
50
  }
51
51
 
52
52
  async fetch(file) {
53
- const name = this._baseUrl + encodePath(file);
54
- console.log("Loading ZIP from " + name);
55
- const response = await fetch(name);
56
- if (!response.ok) throw new Error("Network response was not ok");
53
+ const url = this._baseUrl + encodePath(file);
54
+ console.log("Loading ZIP from " + url);
55
+ const response = await fetch(url);
56
+ if (!response.ok) throw new Error(`Unable to load ${url}, http code ${response.status}`);
57
57
  try {
58
- return (await utils.unzipDiscImage(new Uint8Array(await response.arrayBuffer()))).data;
58
+ return await utils.unzipDiscImage(new Uint8Array(await response.arrayBuffer()));
59
59
  } catch (error) {
60
60
  console.error("Failed to fetch file:", error);
61
61
  throw error;
@@ -35,8 +35,8 @@ Status register:
35
35
  */
36
36
 
37
37
  /**
38
- * Emulates the Acorn teletext adaptor. Dispatches a `showError` CustomEvent, carrying
39
- * `context` and `error` in its detail, when a channel's stream cannot be loaded.
38
+ * Emulates the Acorn teletext adaptor. Dispatches a `notice` CustomEvent, carrying a
39
+ * `message` in its detail, when a channel's stream cannot be loaded.
40
40
  */
41
41
  export class TeletextAdaptor extends EventTarget {
42
42
  constructor(cpu) {
@@ -79,8 +79,10 @@ export class TeletextAdaptor extends EventTarget {
79
79
  if (request !== this.streamRequest) return;
80
80
  console.error(`Teletext adaptor: failed to load channel ${channel}`, error);
81
81
  this.dispatchEvent(
82
- new CustomEvent("showError", {
83
- detail: { context: `loading teletext channel ${channel}`, error },
82
+ new CustomEvent("notice", {
83
+ detail: {
84
+ message: `Teletext channel ${channel} could not be loaded (${error?.message ?? error}). The adaptor carries on with nothing to show.`,
85
+ },
84
86
  }),
85
87
  );
86
88
  return;
package/src/url-params.js CHANGED
@@ -236,6 +236,34 @@ export function processAutobootParams(parsedQuery) {
236
236
  return { needsAutoboot, autoType };
237
237
  }
238
238
 
239
+ /** Where a drive's 40/80 switch is set, `auto` leaving it to whatever disc is loaded. */
240
+ export const DriveTracks = Object.freeze({ auto: "auto", forty: "40", eighty: "80" });
241
+
242
+ const NumDrives = 2;
243
+
244
+ /**
245
+ * Process the per-drive 40/80 track settings
246
+ * @param {Object} parsedQuery - The parsed query parameters
247
+ * @returns {{settings: string[], warnings: string[]}} One DriveTracks per drive, and what was
248
+ * unusable about anything asked for that is not in there
249
+ */
250
+ export function processDriveTrackParams(parsedQuery) {
251
+ const warnings = [];
252
+ const settings = [];
253
+ for (let driveIndex = 0; driveIndex < NumDrives; ++driveIndex) {
254
+ const name = `drive${driveIndex}Tracks`;
255
+ const asked = parsedQuery[name];
256
+ const setting = isDefined(asked) ? `${asked}`.toLowerCase() : DriveTracks.auto;
257
+ if (Object.values(DriveTracks).includes(setting)) {
258
+ settings.push(setting);
259
+ } else {
260
+ warnings.push(`${name}=${asked}: a drive is set to 40, 80 or auto.`);
261
+ settings.push(DriveTracks.auto);
262
+ }
263
+ }
264
+ return { settings, warnings };
265
+ }
266
+
239
267
  /**
240
268
  * Guess the appropriate model based on the hostname
241
269
  * @param {string} hostname - The hostname to check
package/src/utils.js CHANGED
@@ -1216,7 +1216,10 @@ const knownDiscExtensions = {
1216
1216
  uef: true,
1217
1217
  ssd: true,
1218
1218
  dsd: true,
1219
+ adf: true,
1220
+ adm: true,
1219
1221
  adl: true,
1222
+ hfe: true,
1220
1223
  };
1221
1224
 
1222
1225
  const knownRomExtensions = {
package/src/wd-fdc.js CHANGED
@@ -1414,8 +1414,15 @@ export class WdFdc {
1414
1414
  * @param {Number} drive
1415
1415
  * @param {Disc} disc
1416
1416
  */
1417
- loadDisc(drive, disc) {
1417
+ /**
1418
+ * @param {Number} drive
1419
+ * @param {Disc} disc
1420
+ * @param {Number} [tracksPerStep] where to leave the drive's 40/80 switch, which by default
1421
+ * follows the disc, since no drive can tell what pitch the disc in it was written at
1422
+ */
1423
+ loadDisc(drive, disc, tracksPerStep = disc?.is40Track ? 2 : 1) {
1418
1424
  this._drives[drive].setDisc(disc);
1425
+ this._drives[drive].tracksPerStep = tracksPerStep;
1419
1426
  }
1420
1427
 
1421
1428
  get motorOn() {
@@ -15,6 +15,7 @@ export class AudioHandler {
15
15
  this.cpuSpeed = cpuSpeed;
16
16
  this.isAtom = isAtom;
17
17
  this.warningNode = warningNode;
18
+ this.noAudioWorklet = false;
18
19
  toggle(this.warningNode, false);
19
20
  this.stats = {};
20
21
  if (statsNode) {
@@ -41,14 +42,15 @@ export class AudioHandler {
41
42
  } else {
42
43
  if (this.audioContext && !this.audioContext.audioWorklet) {
43
44
  this.audioContext = null;
45
+ this.noAudioWorklet = true;
44
46
  console.log("Unable to initialise audio: no audio worklet API");
45
- toggle(this.warningNode, true);
46
47
  const localhost = new URL(window.location);
47
48
  localhost.hostname = "localhost";
48
49
  this.warningNode.innerHTML = `No audio worklet API was found - there will be no audio.
49
50
  If you are running a local jsbeeb, you must either use a host of
50
51
  <a href="${localhost}">localhost</a>,
51
52
  or serve the content over <em>https</em>.`;
53
+ toggle(this.warningNode, true);
52
54
  }
53
55
  this.soundChip = new FakeSoundChip();
54
56
  this.ddNoise = new FakeDdNoise();
@@ -56,7 +58,6 @@ export class AudioHandler {
56
58
  }
57
59
 
58
60
  this.warningNode.addEventListener("mousedown", () => this.tryResume());
59
- toggle(this.warningNode, false);
60
61
 
61
62
  // Initialise Music 5000 audio context
62
63
  this.audioContextM5000 = createAudioContext({ sampleRate: 46875 });
@@ -145,6 +146,7 @@ export class AudioHandler {
145
146
  }
146
147
 
147
148
  checkStatus() {
149
+ if (this.noAudioWorklet) return;
148
150
  if (!this.audioContext && !this.audioContextM5000) return;
149
151
  const suspended =
150
152
  (this.audioContext && this.audioContext.state === "suspended") ||
@@ -0,0 +1,79 @@
1
+ import * as bootstrap from "bootstrap";
2
+
3
+ /**
4
+ * Passing notices: something happened that is worth knowing and needs nothing doing about it.
5
+ * The error dialog is for the other kind.
6
+ */
7
+
8
+ let nextQuietId = 0;
9
+
10
+ function toastContainer() {
11
+ const existing = document.querySelector(".toast-container");
12
+ if (existing) return existing;
13
+ const container = document.createElement("div");
14
+ container.className = "toast-container position-fixed bottom-0 end-0 p-3";
15
+ document.body.appendChild(container);
16
+ return container;
17
+ }
18
+
19
+ // Storage can be unreachable or full, and a notice that nothing needs doing about is not worth
20
+ // failing over: forget the answer and say the thing again.
21
+ function remembered(key) {
22
+ try {
23
+ return !!window.localStorage.getItem(key);
24
+ } catch (e) {
25
+ console.log(`Unable to read ${key}: ${e}`);
26
+ return false;
27
+ }
28
+ }
29
+
30
+ function remember(key, wanted) {
31
+ try {
32
+ if (wanted) window.localStorage.setItem(key, "yes");
33
+ else window.localStorage.removeItem(key);
34
+ } catch (e) {
35
+ console.log(`Unable to remember ${key}: ${e}`);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * @param {string} message
41
+ * @param {object} [options]
42
+ * @param {string} [options.title] a heading, for a notice whose message does not say where it came from
43
+ * @param {string} [options.quietKey] offer to stop showing this kind of notice, remembering the answer here
44
+ */
45
+ export function toast(message, { title = "", quietKey = "" } = {}) {
46
+ if (quietKey && remembered(quietKey)) return;
47
+
48
+ const element = document.createElement("div");
49
+ element.className = "toast text-bg-dark";
50
+ element.setAttribute("role", "status");
51
+ element.setAttribute("aria-live", "polite");
52
+ element.setAttribute("aria-atomic", "true");
53
+ const quietId = `toast-quiet-${nextQuietId++}`;
54
+ element.innerHTML = `
55
+ <div class="toast-header text-bg-dark">
56
+ <strong class="me-auto"></strong>
57
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
58
+ </div>
59
+ <div class="toast-body">
60
+ <div class="message"></div>
61
+ <div class="form-check mt-2">
62
+ <input class="form-check-input" type="checkbox" id="${quietId}" />
63
+ <label class="form-check-label small" for="${quietId}">Stop telling me this</label>
64
+ </div>
65
+ </div>`;
66
+ // Disc names and the like come from the outside world, so they are set as text, never as markup.
67
+ element.querySelector(".message").textContent = message;
68
+ element.querySelector(".toast-header strong").textContent = title;
69
+ element.querySelector(".toast-header").hidden = !title;
70
+
71
+ const quiet = element.querySelector(".form-check");
72
+ quiet.hidden = !quietKey;
73
+ quiet.querySelector("input").addEventListener("change", (event) => remember(quietKey, event.target.checked));
74
+
75
+ toastContainer().appendChild(element);
76
+ element.addEventListener("hidden.bs.toast", () => element.remove());
77
+ bootstrap.Toast.getOrCreateInstance(element).show();
78
+ return element;
79
+ }
@@ -213,7 +213,7 @@ export class TestMachine {
213
213
 
214
214
  async loadDisc(image) {
215
215
  const data = await fdc.load(image);
216
- this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc, "", data));
216
+ this.processor.fdc.loadDisc(0, fdc.discFor(this.processor.fdc, image, data));
217
217
  }
218
218
 
219
219
  /**