jsbeeb 1.16.0 → 1.17.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 +16 -2
- package/package.json +9 -1
- package/src/6502.js +2 -1
- package/src/app/app.js +4 -0
- package/src/bbcdiscs.js +84 -0
- package/src/bem-snapshot.js +3 -4
- package/src/canvas.js +9 -2
- package/src/cmos.js +38 -0
- package/src/disc-drive.js +45 -12
- package/src/disc-hfe.js +19 -2
- package/src/disc.js +154 -22
- package/src/fdc.js +97 -12
- package/src/google-drive.js +6 -5
- package/src/intel-fdc.js +8 -1
- package/src/jsbeeb.css +65 -0
- package/src/keyboard.js +14 -15
- package/src/machine-session.js +2 -1
- package/src/main.js +381 -170
- package/src/models.js +2 -0
- package/src/printer.js +59 -0
- package/src/sth.js +5 -5
- package/src/teletext_adaptor.js +6 -4
- package/src/url-params.js +28 -0
- package/src/utils.js +28 -55
- package/src/via.js +7 -1
- package/src/wd-fdc.js +8 -1
- package/src/web/audio-handler.js +27 -8
- package/src/web/toast.js +83 -0
- package/tests/test-machine.js +1 -1
package/src/main.js
CHANGED
|
@@ -10,8 +10,9 @@ import { Debugger } from "./web/debug.js";
|
|
|
10
10
|
import { Cpu6502, AtomCpu6502 } from "./6502.js";
|
|
11
11
|
import * as utils_atom from "./utils_atom.js";
|
|
12
12
|
import { LoadSD } from "./mmc.js";
|
|
13
|
-
import { Cmos } from "./cmos.js";
|
|
13
|
+
import { Cmos, localStoragePersistence } from "./cmos.js";
|
|
14
14
|
import { StairwayToHell } from "./sth.js";
|
|
15
|
+
import { BbcDiscArchive, describe as describeHfe } from "./bbcdiscs.js";
|
|
15
16
|
import { GamePad } from "./gamepads.js";
|
|
16
17
|
import * as disc from "./fdc.js";
|
|
17
18
|
import { loadTapeFromData } from "./tapes.js";
|
|
@@ -19,16 +20,18 @@ import { GoogleDriveLoader } from "./google-drive.js";
|
|
|
19
20
|
import * as tokeniser from "./basic-tokenise.js";
|
|
20
21
|
import * as canvasLib from "./canvas.js";
|
|
21
22
|
import { Config } from "./config.js";
|
|
22
|
-
import { tubeModelFor } from "./models.js";
|
|
23
|
+
import { DefaultModel, findModel, tubeModelFor } from "./models.js";
|
|
23
24
|
import { initialise as electron } from "./app/electron.js";
|
|
24
25
|
import { AudioHandler } from "./web/audio-handler.js";
|
|
25
26
|
import { Econet } from "./econet.js";
|
|
26
|
-
import { toSsdOrDsd } from "./disc.js";
|
|
27
|
+
import { DiscLayout, toSsdOrDsd } from "./disc.js";
|
|
27
28
|
import { toHfe } from "./disc-hfe.js";
|
|
28
29
|
import { Keyboard } from "./keyboard.js";
|
|
29
30
|
import { GamepadSource } from "./gamepad-source.js";
|
|
31
|
+
import { toast } from "./web/toast.js";
|
|
30
32
|
import { MicrophoneInput } from "./microphone-input.js";
|
|
31
33
|
import { SpeechOutput } from "./speech-output.js";
|
|
34
|
+
import { Printer } from "./printer.js";
|
|
32
35
|
import { MouseJoystickSource } from "./mouse-joystick-source.js";
|
|
33
36
|
import { calculateMouseCoordinates } from "./mouse-coordinates.js";
|
|
34
37
|
import { getFilterForMode } from "./canvas.js";
|
|
@@ -48,11 +51,13 @@ import { DiscVisualiser } from "./disc-visualiser.js";
|
|
|
48
51
|
import { downloadBlob } from "./dom-utils.js";
|
|
49
52
|
import {
|
|
50
53
|
buildUrlFromParams,
|
|
54
|
+
DriveTracks,
|
|
51
55
|
guessModelFromHostname,
|
|
52
56
|
ParamTypes,
|
|
53
57
|
parseMediaParams,
|
|
54
58
|
parseQueryString,
|
|
55
59
|
processAutobootParams,
|
|
60
|
+
processDriveTrackParams,
|
|
56
61
|
processInputParams,
|
|
57
62
|
} from "./url-params.js";
|
|
58
63
|
|
|
@@ -65,6 +70,7 @@ let frameSkip = 0;
|
|
|
65
70
|
let syncLights;
|
|
66
71
|
let discSth;
|
|
67
72
|
let tapeSth;
|
|
73
|
+
let hfeArchive;
|
|
68
74
|
let running;
|
|
69
75
|
let model;
|
|
70
76
|
|
|
@@ -153,6 +159,8 @@ const paramTypes = {
|
|
|
153
159
|
keyLayout: ParamTypes.STRING,
|
|
154
160
|
autotype: ParamTypes.STRING,
|
|
155
161
|
displayMode: ParamTypes.STRING,
|
|
162
|
+
drive0Tracks: ParamTypes.STRING,
|
|
163
|
+
drive1Tracks: ParamTypes.STRING,
|
|
156
164
|
};
|
|
157
165
|
|
|
158
166
|
// Parse the query string with parameter types
|
|
@@ -173,6 +181,7 @@ let econet = null;
|
|
|
173
181
|
|
|
174
182
|
// Parse disc and tape images from query parameters
|
|
175
183
|
const { discImage: queryDiscImage, secondDiscImage: querySecondDisc, mmcImage } = parseMediaParams(parsedQuery);
|
|
184
|
+
const { settings: driveTracks, warnings: driveTrackWarnings } = processDriveTrackParams(parsedQuery);
|
|
176
185
|
|
|
177
186
|
// Only assign if values are provided
|
|
178
187
|
if (queryDiscImage) discImage = queryDiscImage;
|
|
@@ -200,19 +209,16 @@ if (parsedQuery.audiofilterq !== undefined) audioFilterQ = parsedQuery.audiofilt
|
|
|
200
209
|
if (parsedQuery.stationId !== undefined) stationId = parsedQuery.stationId;
|
|
201
210
|
if (parsedQuery.frameSkip !== undefined) frameSkip = parsedQuery.frameSkip;
|
|
202
211
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
if (!output || level) return;
|
|
207
|
-
|
|
208
|
-
const uservia = processor.uservia;
|
|
209
|
-
// Ack the character by pulsing CA1 low.
|
|
210
|
-
uservia.setca1(false);
|
|
211
|
-
uservia.setca1(true);
|
|
212
|
-
const newChar = String.fromCharCode(uservia.ora);
|
|
213
|
-
printerTextArea.value += newChar;
|
|
212
|
+
const printer = new Printer({
|
|
213
|
+
onOutput: (char) => {
|
|
214
|
+
if (printerTextArea) printerTextArea.value += char;
|
|
214
215
|
},
|
|
215
|
-
|
|
216
|
+
onFirstOutput: () =>
|
|
217
|
+
toast("Printer output is being kept. Press Ctrl-B to open the printer window.", {
|
|
218
|
+
title: "Printer",
|
|
219
|
+
quietKey: "quietPrinterOutput",
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
216
222
|
|
|
217
223
|
// Accessibility switch state — bits 0-7 correspond to switches 1-8.
|
|
218
224
|
// Active low: 0xff = no switches pressed; clearing a bit = that switch is pressed.
|
|
@@ -282,7 +288,13 @@ const config = new Config(
|
|
|
282
288
|
// Perform mapping of legacy models to the new format
|
|
283
289
|
config.mapLegacyModels(parsedQuery);
|
|
284
290
|
|
|
285
|
-
|
|
291
|
+
const requestedModelName = parsedQuery.model || guessModelFromHostname(window.location.hostname);
|
|
292
|
+
const requestedModel = findModel(requestedModelName);
|
|
293
|
+
if (!requestedModel)
|
|
294
|
+
toast(`There is no model called "${requestedModelName}". Using ${DefaultModel.name} instead.`, {
|
|
295
|
+
title: "Model",
|
|
296
|
+
});
|
|
297
|
+
config.setModel((requestedModel ?? DefaultModel).name);
|
|
286
298
|
config.setKeyLayout(keyLayout);
|
|
287
299
|
config.setTubeCpuMultiplier(parsedQuery.tubeCpuMultiplier || 1);
|
|
288
300
|
config.setMicrophoneChannel(parsedQuery.microphoneChannel);
|
|
@@ -321,7 +333,7 @@ const emulationConfig = {
|
|
|
321
333
|
// before any the user asked for with ?rom=.
|
|
322
334
|
extraRoms: [...config.extraRoms, ...extraRoms],
|
|
323
335
|
userPort,
|
|
324
|
-
printerPort,
|
|
336
|
+
printerPort: printer,
|
|
325
337
|
getGamepads: function () {
|
|
326
338
|
// Gamepads are only available in secure contexts. If e.g. loading from http:// urls they aren't there.
|
|
327
339
|
return navigator.getGamepads ? navigator.getGamepads() : [];
|
|
@@ -383,8 +395,80 @@ function showError(context, error) {
|
|
|
383
395
|
errorDialogModal.show();
|
|
384
396
|
}
|
|
385
397
|
|
|
398
|
+
const errorText = (error) => error?.message ?? `${error}`;
|
|
399
|
+
|
|
400
|
+
function showNotice(event) {
|
|
401
|
+
const { message, title, quietKey } = event.detail;
|
|
402
|
+
toast(message, { title, quietKey });
|
|
403
|
+
}
|
|
404
|
+
|
|
386
405
|
if (keyMappingWarnings.length) {
|
|
387
|
-
|
|
406
|
+
toast(`${keyMappingWarnings.join(" ")} The key names are listed in the README.`, {
|
|
407
|
+
title: "Mappings in the URL",
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (driveTrackWarnings.length) {
|
|
412
|
+
toast(`${driveTrackWarnings.join(" ")} Auto is in use instead; pick 40 or 80 from the Discs menu.`, {
|
|
413
|
+
title: "Disc drives",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** @returns {string} the DiscLayout to load an image for this drive with */
|
|
418
|
+
function layoutForDrive(driveIndex) {
|
|
419
|
+
return driveTracks[driveIndex] === DriveTracks.eighty ? DiscLayout.contiguous : DiscLayout.auto;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** @returns {Number|undefined} the tracksPerStep the user fixed this drive at, if they fixed one */
|
|
423
|
+
function tracksPerStepForDrive(driveIndex) {
|
|
424
|
+
if (driveTracks[driveIndex] === DriveTracks.auto) return undefined;
|
|
425
|
+
return driveTracks[driveIndex] === DriveTracks.forty ? 2 : 1;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function putDiscIn(driveIndex, loadedDisc) {
|
|
429
|
+
const drive = processor.fdc.drives[driveIndex];
|
|
430
|
+
const fixed = tracksPerStepForDrive(driveIndex);
|
|
431
|
+
const was = drive.tracksPerStep;
|
|
432
|
+
processor.fdc.loadDisc(driveIndex, loadedDisc, fixed);
|
|
433
|
+
showDriveTracks(driveIndex);
|
|
434
|
+
noteUnsavedWrites(loadedDisc);
|
|
435
|
+
// A switch the user fixed does not move, so anything it does is not news.
|
|
436
|
+
if (fixed === undefined && drive.tracksPerStep !== was) noteDriveTracks(driveIndex, loadedDisc.name);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let saidWritesAreNotKept = false;
|
|
440
|
+
|
|
441
|
+
function noteUnsavedWrites(loadedDisc) {
|
|
442
|
+
if (loadedDisc.savesChanges || saidWritesAreNotKept) return;
|
|
443
|
+
loadedDisc.notifyOnFirstTrackWrite(() => {
|
|
444
|
+
if (saidWritesAreNotKept) return;
|
|
445
|
+
saidWritesAreNotKept = true;
|
|
446
|
+
toast(`Changes to ${loadedDisc.name} are not saved. Use Discs, Download to keep a copy.`, {
|
|
447
|
+
title: "Disc",
|
|
448
|
+
quietKey: "quietDiscNotSaved",
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const tracksPerStepFor = (tracks) => (tracks === "40" ? 2 : 1);
|
|
454
|
+
|
|
455
|
+
function showDriveTracks(driveIndex) {
|
|
456
|
+
const drive = processor.fdc?.drives[driveIndex];
|
|
457
|
+
if (!drive) return;
|
|
458
|
+
for (const button of driveTracksButtons(driveIndex))
|
|
459
|
+
button.classList.toggle("active", tracksPerStepFor(button.dataset.tracks) === drive.tracksPerStep);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function driveTracksButtons(driveIndex) {
|
|
463
|
+
return document.querySelectorAll(`.drive-tracks[data-drive="${driveIndex}"] [data-tracks]`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function noteDriveTracks(driveIndex, discName) {
|
|
467
|
+
const tracks = processor.fdc.drives[driveIndex].tracksPerStep === 2 ? "40" : "80";
|
|
468
|
+
toast(`Drive ${driveIndex} switched to ${tracks} track for ${discName}.`, {
|
|
469
|
+
title: "Disc drive",
|
|
470
|
+
quietKey: "quietDriveTracks",
|
|
471
|
+
});
|
|
388
472
|
}
|
|
389
473
|
|
|
390
474
|
function createCanvasForFilter(filterClass) {
|
|
@@ -402,10 +486,11 @@ function createCanvasForFilter(filterClass) {
|
|
|
402
486
|
// filter can decline a context that works perfectly well for other modes,
|
|
403
487
|
// in which case bestCanvas quietly gives us an unfiltered GL canvas.
|
|
404
488
|
if (newCanvas.filterClass !== filterClass) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
489
|
+
const reason = newCanvas.fallbackReason ? ` (${newCanvas.fallbackReason})` : "";
|
|
490
|
+
toast(`${displayConfig.name} is not available on this device, so the standard display is in use${reason}.`, {
|
|
491
|
+
title: "Display",
|
|
492
|
+
quietKey: "quietDisplayFallback",
|
|
493
|
+
});
|
|
409
494
|
}
|
|
410
495
|
|
|
411
496
|
return newCanvas;
|
|
@@ -516,10 +601,10 @@ function downloadDriveData(data, name, extension) {
|
|
|
516
601
|
|
|
517
602
|
async function loadHTMLFile(file) {
|
|
518
603
|
const imageData = utils.stringToUint8Array(await readFileAsBinaryString(file));
|
|
519
|
-
const loadedDisc = disc.discFor(processor.fdc, file.name, imageData);
|
|
604
|
+
const loadedDisc = disc.discFor(processor.fdc, file.name, imageData, undefined, layoutForDrive(0));
|
|
520
605
|
// Local file: retain the image bytes for embedding in save-to-file snapshots.
|
|
521
606
|
loadedDisc.setOriginalImage(imageData);
|
|
522
|
-
|
|
607
|
+
putDiscIn(0, loadedDisc);
|
|
523
608
|
delete parsedQuery.disc;
|
|
524
609
|
delete parsedQuery.disc1;
|
|
525
610
|
updateUrl();
|
|
@@ -653,17 +738,14 @@ if (config.hasEconet) {
|
|
|
653
738
|
}
|
|
654
739
|
|
|
655
740
|
const cmos = new Cmos(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
window.localStorage.cmosRam = JSON.stringify(data);
|
|
665
|
-
},
|
|
666
|
-
},
|
|
741
|
+
localStoragePersistence(
|
|
742
|
+
() => window.localStorage,
|
|
743
|
+
(error) =>
|
|
744
|
+
toast(
|
|
745
|
+
`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.`,
|
|
746
|
+
{ title: "Settings", quietKey: "quietCmosSave" },
|
|
747
|
+
),
|
|
748
|
+
),
|
|
667
749
|
model.cmosOverride,
|
|
668
750
|
econet,
|
|
669
751
|
);
|
|
@@ -675,12 +757,17 @@ function checkPrinterWindow() {
|
|
|
675
757
|
if (printerWindow && !printerWindow.closed) return;
|
|
676
758
|
|
|
677
759
|
printerWindow = window.open("", "_blank", "height=300,width=400");
|
|
760
|
+
if (!printerWindow) {
|
|
761
|
+
toast("The printer output window was blocked. Allow pop-up windows for this site, then press Ctrl-B again.", {
|
|
762
|
+
title: "Printer",
|
|
763
|
+
});
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
678
766
|
printerWindow.document.write(
|
|
679
767
|
'<textarea id="text" rows="15" cols="40" placeholder="Printer outputs here..."></textarea>',
|
|
680
768
|
);
|
|
681
769
|
printerTextArea = printerWindow.document.getElementById("text");
|
|
682
|
-
|
|
683
|
-
processor.uservia.setca1(true);
|
|
770
|
+
printerTextArea.value = printer.text;
|
|
684
771
|
}
|
|
685
772
|
|
|
686
773
|
const CpuClass = model.isAtom ? AtomCpu6502 : Cpu6502;
|
|
@@ -696,14 +783,16 @@ processor = new CpuClass(model, {
|
|
|
696
783
|
econet,
|
|
697
784
|
});
|
|
698
785
|
|
|
699
|
-
|
|
786
|
+
printer.attach(processor.uservia);
|
|
787
|
+
|
|
788
|
+
processor.teletextAdaptor?.addEventListener("notice", showNotice);
|
|
700
789
|
|
|
701
790
|
// Create input sources
|
|
702
791
|
const gamepadSource = new GamepadSource(emulationConfig.getGamepads);
|
|
703
792
|
// Create MicrophoneInput but don't enable by default
|
|
704
793
|
const microphoneInput = new MicrophoneInput();
|
|
705
794
|
microphoneInput.setErrorCallback((message) => {
|
|
706
|
-
|
|
795
|
+
toast(`${message} The microphone channel has been turned off.`, { title: "Microphone" });
|
|
707
796
|
});
|
|
708
797
|
|
|
709
798
|
// Create MouseJoystickSource but don't enable by default
|
|
@@ -804,7 +893,7 @@ keyboard = new Keyboard({
|
|
|
804
893
|
keyLayout,
|
|
805
894
|
dbgr,
|
|
806
895
|
});
|
|
807
|
-
keyboard.addEventListener("
|
|
896
|
+
keyboard.addEventListener("notice", showNotice);
|
|
808
897
|
keyboard.addEventListener("pause", () => stop(false));
|
|
809
898
|
keyboard.addEventListener("resume", () => go());
|
|
810
899
|
keyboard.addEventListener("break", (e) => {
|
|
@@ -939,15 +1028,26 @@ function setTapeImage(name) {
|
|
|
939
1028
|
config.dispatchEvent(new CustomEvent("media-changed", { detail: { tape: name } }));
|
|
940
1029
|
}
|
|
941
1030
|
|
|
942
|
-
function
|
|
943
|
-
for (const el of document.querySelectorAll(
|
|
1031
|
+
function clearArchiveList(listId) {
|
|
1032
|
+
for (const el of document.querySelectorAll(`#${listId} li:not(.template)`)) el.remove();
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function showArchiveMessage(modalId, listId, message) {
|
|
1036
|
+
const loading = document.querySelector(`#${modalId} .loading`);
|
|
1037
|
+
loading.textContent = message;
|
|
1038
|
+
loading.style.display = "";
|
|
1039
|
+
clearArchiveList(listId);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function filterArchiveList(listId, filter) {
|
|
1043
|
+
filter = filter.toLowerCase();
|
|
1044
|
+
for (const el of document.querySelectorAll(`#${listId} li:not(.template)`)) {
|
|
1045
|
+
el.style.display = el.textContent.toLowerCase().includes(filter) ? "" : "none";
|
|
1046
|
+
}
|
|
944
1047
|
}
|
|
945
1048
|
|
|
946
1049
|
function sthStartLoad() {
|
|
947
|
-
|
|
948
|
-
sthLoading.textContent = "Loading catalog from STH archive";
|
|
949
|
-
sthLoading.style.display = "";
|
|
950
|
-
sthClearList();
|
|
1050
|
+
showArchiveMessage("sth", "sth-list", "Loading catalog from STH archive");
|
|
951
1051
|
}
|
|
952
1052
|
|
|
953
1053
|
async function discSthClick(item) {
|
|
@@ -960,8 +1060,8 @@ async function discSthClick(item) {
|
|
|
960
1060
|
|
|
961
1061
|
popupLoading("Loading " + item);
|
|
962
1062
|
try {
|
|
963
|
-
const
|
|
964
|
-
|
|
1063
|
+
const loaded = await loadDiscImage(parsedQuery.disc1, layoutForDrive(0));
|
|
1064
|
+
putDiscIn(0, loaded);
|
|
965
1065
|
loadingFinished();
|
|
966
1066
|
|
|
967
1067
|
if (needsAutoboot) {
|
|
@@ -969,7 +1069,7 @@ async function discSthClick(item) {
|
|
|
969
1069
|
}
|
|
970
1070
|
} catch (err) {
|
|
971
1071
|
console.error("Error loading disc image:", err);
|
|
972
|
-
loadingFinished(err);
|
|
1072
|
+
loadingFinished(`Unable to load ${item} from the STH archive: ${errorText(err)}`);
|
|
973
1073
|
}
|
|
974
1074
|
}
|
|
975
1075
|
|
|
@@ -984,7 +1084,7 @@ async function tapeSthClick(item) {
|
|
|
984
1084
|
loadingFinished();
|
|
985
1085
|
} catch (err) {
|
|
986
1086
|
console.error("Error loading tape image:", err);
|
|
987
|
-
loadingFinished(err);
|
|
1087
|
+
loadingFinished(`Unable to load ${item} from the STH archive: ${errorText(err)}`);
|
|
988
1088
|
}
|
|
989
1089
|
}
|
|
990
1090
|
|
|
@@ -995,7 +1095,7 @@ document.getElementById("sth").addEventListener("shown.bs.modal", () => {
|
|
|
995
1095
|
|
|
996
1096
|
function makeOnCat(onClick) {
|
|
997
1097
|
return function (cat) {
|
|
998
|
-
|
|
1098
|
+
clearArchiveList("sth-list");
|
|
999
1099
|
const sthList = document.getElementById("sth-list");
|
|
1000
1100
|
document.querySelector("#sth .loading").style.display = "none";
|
|
1001
1101
|
const template = sthList.querySelector(".template");
|
|
@@ -1025,24 +1125,30 @@ function makeOnCat(onClick) {
|
|
|
1025
1125
|
}
|
|
1026
1126
|
|
|
1027
1127
|
function sthOnError() {
|
|
1028
|
-
|
|
1029
|
-
sthLoading.textContent = "There was an error accessing the STH archive";
|
|
1030
|
-
sthLoading.style.display = "";
|
|
1031
|
-
sthClearList();
|
|
1128
|
+
showArchiveMessage("sth", "sth-list", "There was an error accessing the STH archive");
|
|
1032
1129
|
}
|
|
1033
1130
|
|
|
1034
1131
|
discSth = new StairwayToHell(sthStartLoad, makeOnCat(discSthClick), sthOnError, false);
|
|
1035
1132
|
tapeSth = new StairwayToHell(sthStartLoad, makeOnCat(tapeSthClick), sthOnError, true);
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1133
|
+
hfeArchive = new BbcDiscArchive(hfeStartLoad, hfeOnCat, hfeOnError);
|
|
1134
|
+
|
|
1135
|
+
// Every archive picker offers the same autoboot choice, and it is one setting,
|
|
1136
|
+
// so ticking it in either has to show in both.
|
|
1137
|
+
const autobootChecks = document.querySelectorAll("#sth .autoboot, #hfe .autoboot");
|
|
1138
|
+
function showAutoboot(checked) {
|
|
1139
|
+
for (const check of autobootChecks) check.checked = checked;
|
|
1140
|
+
}
|
|
1141
|
+
for (const check of autobootChecks) {
|
|
1142
|
+
check.addEventListener("click", function () {
|
|
1143
|
+
showAutoboot(check.checked);
|
|
1144
|
+
if (check.checked) {
|
|
1145
|
+
parsedQuery.autoboot = "";
|
|
1146
|
+
} else {
|
|
1147
|
+
delete parsedQuery.autoboot;
|
|
1148
|
+
}
|
|
1149
|
+
updateUrl();
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1046
1152
|
|
|
1047
1153
|
document.addEventListener("click", function (e) {
|
|
1048
1154
|
const target = e.target.closest("a.sth");
|
|
@@ -1058,16 +1164,94 @@ document.addEventListener("click", function (e) {
|
|
|
1058
1164
|
});
|
|
1059
1165
|
|
|
1060
1166
|
function setSthFilter(filter) {
|
|
1061
|
-
|
|
1062
|
-
for (const el of document.querySelectorAll("#sth-list li:not(.template)")) {
|
|
1063
|
-
el.style.display = el.textContent.toLowerCase().indexOf(filter) >= 0 ? "" : "none";
|
|
1064
|
-
}
|
|
1167
|
+
filterArchiveList("sth-list", filter);
|
|
1065
1168
|
}
|
|
1066
1169
|
|
|
1067
1170
|
const sthFilter = document.getElementById("sth-filter");
|
|
1068
1171
|
sthFilter.addEventListener("change", () => setSthFilter(sthFilter.value));
|
|
1069
1172
|
sthFilter.addEventListener("keyup", () => setSthFilter(sthFilter.value));
|
|
1070
1173
|
|
|
1174
|
+
// Rendering is spread over several turns of the event loop, so a list that has
|
|
1175
|
+
// been emptied may still have a chain of appends heading for it. Anything that
|
|
1176
|
+
// clears the list takes a new ticket; a chain whose ticket is stale gives up.
|
|
1177
|
+
let hfeRender = 0;
|
|
1178
|
+
|
|
1179
|
+
function hfeStartLoad() {
|
|
1180
|
+
hfeRender++;
|
|
1181
|
+
showArchiveMessage("hfe", "hfe-list", "Loading catalogue from HFE archive");
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function hfeOnError() {
|
|
1185
|
+
hfeRender++;
|
|
1186
|
+
showArchiveMessage("hfe", "hfe-list", "There was an error accessing the HFE archive");
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
async function hfeClick(file) {
|
|
1190
|
+
utils.noteEvent("hfe", "click", file.path);
|
|
1191
|
+
setDisc1Image("hfe:" + file.path);
|
|
1192
|
+
const needsAutoboot = parsedQuery.autoboot !== undefined;
|
|
1193
|
+
if (needsAutoboot) processor.reset(true);
|
|
1194
|
+
|
|
1195
|
+
const name = describeHfe(file).title;
|
|
1196
|
+
popupLoading("Loading " + name);
|
|
1197
|
+
try {
|
|
1198
|
+
const loaded = await loadDiscImage(parsedQuery.disc1, layoutForDrive(0));
|
|
1199
|
+
putDiscIn(0, loaded);
|
|
1200
|
+
loadingFinished();
|
|
1201
|
+
if (needsAutoboot) autoboot(name);
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
console.error("Error loading disc image:", err);
|
|
1204
|
+
loadingFinished(`Unable to load ${name} from the HFE archive: ${errorText(err)}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function hfeOnCat(catalogue) {
|
|
1209
|
+
const ticket = ++hfeRender;
|
|
1210
|
+
clearArchiveList("hfe-list");
|
|
1211
|
+
const list = document.getElementById("hfe-list");
|
|
1212
|
+
document.querySelector("#hfe .loading").style.display = "none";
|
|
1213
|
+
const template = list.querySelector(".template");
|
|
1214
|
+
|
|
1215
|
+
const addSome = (remaining) => {
|
|
1216
|
+
if (ticket !== hfeRender) return;
|
|
1217
|
+
const MaxAtATime = 100;
|
|
1218
|
+
const Delay = 30;
|
|
1219
|
+
// Read per batch: the filter can be typed into while this is still going.
|
|
1220
|
+
const filter = document.getElementById("hfe-filter").value.toLowerCase();
|
|
1221
|
+
for (const file of remaining.slice(0, MaxAtATime)) {
|
|
1222
|
+
const { title, publisher, detail } = describeHfe(file);
|
|
1223
|
+
const row = template.cloneNode(true);
|
|
1224
|
+
row.classList.remove("template");
|
|
1225
|
+
row.querySelector(".name").textContent = title;
|
|
1226
|
+
row.querySelector(".publisher").textContent = publisher;
|
|
1227
|
+
row.querySelector(".detail").textContent = detail;
|
|
1228
|
+
if (file.notes) row.title = file.notes;
|
|
1229
|
+
// The row is an anchor, and letting it navigate to "#" would push a
|
|
1230
|
+
// history entry of its own on top of the one updateUrl pushes.
|
|
1231
|
+
row.addEventListener("click", (event) => {
|
|
1232
|
+
event.preventDefault();
|
|
1233
|
+
hfeClick(file);
|
|
1234
|
+
$hfeModal.hide();
|
|
1235
|
+
});
|
|
1236
|
+
row.style.display = row.textContent.toLowerCase().includes(filter) ? "" : "none";
|
|
1237
|
+
list.appendChild(row);
|
|
1238
|
+
}
|
|
1239
|
+
if (remaining.length > MaxAtATime) setTimeout(() => addSome(remaining.slice(MaxAtATime)), Delay);
|
|
1240
|
+
};
|
|
1241
|
+
addSome(catalogue);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const $hfeModal = new bootstrap.Modal(document.getElementById("hfe"));
|
|
1245
|
+
document.getElementById("hfe").addEventListener("shown.bs.modal", () => {
|
|
1246
|
+
document.getElementById("hfe-filter").focus();
|
|
1247
|
+
});
|
|
1248
|
+
document.getElementById("hfe").addEventListener("show.bs.modal", () => hfeArchive.populate());
|
|
1249
|
+
|
|
1250
|
+
const hfeFilter = document.getElementById("hfe-filter");
|
|
1251
|
+
const onHfeFilter = () => filterArchiveList("hfe-list", hfeFilter.value);
|
|
1252
|
+
hfeFilter.addEventListener("change", onHfeFilter);
|
|
1253
|
+
hfeFilter.addEventListener("keyup", onHfeFilter);
|
|
1254
|
+
|
|
1071
1255
|
function sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
|
|
1072
1256
|
if (keyboard) {
|
|
1073
1257
|
keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
|
|
@@ -1138,10 +1322,13 @@ async function reloadSnapshotMedia(media) {
|
|
|
1138
1322
|
const imageDataKey = discKey + "ImageData";
|
|
1139
1323
|
const crcKey = discKey + "Crc32";
|
|
1140
1324
|
|
|
1325
|
+
// A snapshot from before layout detection has no field, and was contiguous.
|
|
1326
|
+
const layout = media[discKey + "Layout"] ?? DiscLayout.contiguous;
|
|
1327
|
+
|
|
1141
1328
|
let loadedDisc = null;
|
|
1142
1329
|
if (media[discKey]) {
|
|
1143
1330
|
// URL-based disc — reload from source
|
|
1144
|
-
loadedDisc = await loadDiscImage(media[discKey]);
|
|
1331
|
+
loadedDisc = await loadDiscImage(media[discKey], layout);
|
|
1145
1332
|
} else if (media[imageDataKey]) {
|
|
1146
1333
|
// Locally-loaded disc — reconstruct from embedded image data
|
|
1147
1334
|
const imageData =
|
|
@@ -1149,7 +1336,7 @@ async function reloadSnapshotMedia(media) {
|
|
|
1149
1336
|
? media[imageDataKey]
|
|
1150
1337
|
: new Uint8Array(Object.values(media[imageDataKey]));
|
|
1151
1338
|
const discName = media[discKey + "Name"] || "snapshot.ssd";
|
|
1152
|
-
loadedDisc = disc.discFor(processor.fdc, discName, imageData);
|
|
1339
|
+
loadedDisc = disc.discFor(processor.fdc, discName, imageData, undefined, layout);
|
|
1153
1340
|
// Retain the image bytes so subsequent saves can re-embed them.
|
|
1154
1341
|
loadedDisc.setOriginalImage(imageData);
|
|
1155
1342
|
}
|
|
@@ -1158,14 +1345,14 @@ async function reloadSnapshotMedia(media) {
|
|
|
1158
1345
|
// Verify CRC32 if present
|
|
1159
1346
|
if (media[crcKey] != null && loadedDisc.originalImageCrc32 != null) {
|
|
1160
1347
|
if (loadedDisc.originalImageCrc32 !== media[crcKey]) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1348
|
+
toast(
|
|
1349
|
+
`${loadedDisc.name} has changed since this state was saved. The state has been restored anyway and may not run correctly.`,
|
|
1350
|
+
{ title: "Restoring state" },
|
|
1164
1351
|
);
|
|
1165
1352
|
}
|
|
1166
1353
|
}
|
|
1167
1354
|
|
|
1168
|
-
|
|
1355
|
+
putDiscIn(driveIndex, loadedDisc);
|
|
1169
1356
|
// Only update the URL/query for URL-sourced discs. For embedded
|
|
1170
1357
|
// (local-file) discs, setting parsedQuery would put a bogus source
|
|
1171
1358
|
// in the URL and break subsequent saves/reloads.
|
|
@@ -1176,13 +1363,18 @@ async function reloadSnapshotMedia(media) {
|
|
|
1176
1363
|
}
|
|
1177
1364
|
}
|
|
1178
1365
|
|
|
1179
|
-
async function loadDiscImage(discImage) {
|
|
1366
|
+
async function loadDiscImage(discImage, layout = DiscLayout.auto) {
|
|
1180
1367
|
if (!discImage) return null;
|
|
1181
1368
|
const split = splitImage(discImage);
|
|
1182
1369
|
discImage = split.image;
|
|
1183
1370
|
const schema = split.schema;
|
|
1184
1371
|
if (schema[0] === "!" || schema === "local") {
|
|
1185
|
-
return disc.localDisc(processor.fdc, discImage)
|
|
1372
|
+
return disc.localDisc(processor.fdc, discImage, layout, (error) =>
|
|
1373
|
+
toast(
|
|
1374
|
+
`Browser storage would not take changes to ${discImage} (${errorText(error)}). Use Discs, Download to keep a copy.`,
|
|
1375
|
+
{ title: "Disc", quietKey: "quietLocalDiscSaveFailed" },
|
|
1376
|
+
),
|
|
1377
|
+
);
|
|
1186
1378
|
}
|
|
1187
1379
|
// TODO: come up with a decent UX for passing an 'onChange' parameter to each of these.
|
|
1188
1380
|
// Consider:
|
|
@@ -1192,8 +1384,13 @@ async function loadDiscImage(discImage) {
|
|
|
1192
1384
|
// * Dialog box (ugh) saying "is this ok?"
|
|
1193
1385
|
switch (schema) {
|
|
1194
1386
|
case "|":
|
|
1195
|
-
case "sth":
|
|
1196
|
-
|
|
1387
|
+
case "sth": {
|
|
1388
|
+
const { name, data } = await discSth.fetch(discImage);
|
|
1389
|
+
return disc.discFor(processor.fdc, name, data, undefined, layout);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
case "hfe":
|
|
1393
|
+
return disc.discFor(processor.fdc, discImage, await hfeArchive.fetch(discImage), undefined, layout);
|
|
1197
1394
|
|
|
1198
1395
|
case "gd": {
|
|
1199
1396
|
const splat = discImage.match(/([^/]+)\/?(.*)/);
|
|
@@ -1202,15 +1399,15 @@ async function loadDiscImage(discImage) {
|
|
|
1202
1399
|
discImage = splat[1];
|
|
1203
1400
|
name = splat[2];
|
|
1204
1401
|
}
|
|
1205
|
-
return gdLoad({ name, id: discImage });
|
|
1402
|
+
return gdLoad({ name, id: discImage }, layout);
|
|
1206
1403
|
}
|
|
1207
1404
|
case "b64data":
|
|
1208
|
-
return disc.discFor(processor.fdc, "disk.ssd", atob(discImage));
|
|
1405
|
+
return disc.discFor(processor.fdc, "disk.ssd", atob(discImage), undefined, layout);
|
|
1209
1406
|
|
|
1210
1407
|
case "data": {
|
|
1211
1408
|
const arr = Array.prototype.map.call(atob(discImage), (x) => x.charCodeAt(0));
|
|
1212
1409
|
const { name, data } = await utils.unzipDiscImage(arr);
|
|
1213
|
-
return disc.discFor(processor.fdc, name, data);
|
|
1410
|
+
return disc.discFor(processor.fdc, name, data, undefined, layout);
|
|
1214
1411
|
}
|
|
1215
1412
|
case "http":
|
|
1216
1413
|
case "https":
|
|
@@ -1224,10 +1421,10 @@ async function loadDiscImage(discImage) {
|
|
|
1224
1421
|
discData = unzipped.data;
|
|
1225
1422
|
discImage = unzipped.name;
|
|
1226
1423
|
}
|
|
1227
|
-
return disc.discFor(processor.fdc, discImage, discData);
|
|
1424
|
+
return disc.discFor(processor.fdc, discImage, discData, undefined, layout);
|
|
1228
1425
|
}
|
|
1229
1426
|
default:
|
|
1230
|
-
return disc.discFor(processor.fdc, discImage, await disc.load("discs/" + discImage));
|
|
1427
|
+
return disc.discFor(processor.fdc, discImage, await disc.load("discs/" + discImage), undefined, layout);
|
|
1231
1428
|
}
|
|
1232
1429
|
}
|
|
1233
1430
|
|
|
@@ -1238,8 +1435,10 @@ async function loadTapeImage(tapeImage) {
|
|
|
1238
1435
|
|
|
1239
1436
|
switch (schema) {
|
|
1240
1437
|
case "|":
|
|
1241
|
-
case "sth":
|
|
1242
|
-
|
|
1438
|
+
case "sth": {
|
|
1439
|
+
const { name, data } = await tapeSth.fetch(tapeImage);
|
|
1440
|
+
return await loadTapeFromData(name, data, model);
|
|
1441
|
+
}
|
|
1243
1442
|
|
|
1244
1443
|
case "data": {
|
|
1245
1444
|
const arr = Array.prototype.map.call(atob(tapeImage), (x) => x.charCodeAt(0));
|
|
@@ -1337,17 +1536,10 @@ function popupLoading(msg) {
|
|
|
1337
1536
|
loadingDialogModal.show();
|
|
1338
1537
|
}
|
|
1339
1538
|
|
|
1340
|
-
function loadingFinished(
|
|
1539
|
+
function loadingFinished(message) {
|
|
1341
1540
|
googleDriveAuth.style.display = "none";
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
loadingDialog.querySelector(".loading").textContent = "Error: " + error;
|
|
1345
|
-
setTimeout(function () {
|
|
1346
|
-
loadingDialogModal.hide();
|
|
1347
|
-
}, 5000);
|
|
1348
|
-
} else {
|
|
1349
|
-
loadingDialogModal.hide();
|
|
1350
|
-
}
|
|
1541
|
+
loadingDialogModal.hide();
|
|
1542
|
+
if (message) toast(message);
|
|
1351
1543
|
}
|
|
1352
1544
|
|
|
1353
1545
|
const googleDrive = new GoogleDriveLoader();
|
|
@@ -1372,7 +1564,7 @@ document.querySelector("#google-drive-auth form").addEventListener("submit", asy
|
|
|
1372
1564
|
else googleDriveLoadingReject(new Error("Unable to authorize Google Drive"));
|
|
1373
1565
|
});
|
|
1374
1566
|
|
|
1375
|
-
async function gdLoad(cat) {
|
|
1567
|
+
async function gdLoad(cat, layout) {
|
|
1376
1568
|
// TODO: have a onclose flush event, handle errors
|
|
1377
1569
|
/*
|
|
1378
1570
|
$(window).bind("beforeunload", function() {
|
|
@@ -1396,22 +1588,26 @@ async function gdLoad(cat) {
|
|
|
1396
1588
|
});
|
|
1397
1589
|
}
|
|
1398
1590
|
|
|
1399
|
-
const ssd = await googleDrive.load(processor.fdc, cat.id);
|
|
1591
|
+
const ssd = await googleDrive.load(processor.fdc, cat.id, layout);
|
|
1400
1592
|
console.log("Google Drive loading finished");
|
|
1401
1593
|
loadingFinished();
|
|
1402
1594
|
return ssd;
|
|
1403
1595
|
} catch (error) {
|
|
1404
1596
|
console.error("Google Drive loading error:", error);
|
|
1405
|
-
loadingFinished(error);
|
|
1597
|
+
loadingFinished(`Unable to load ${cat.name} from Google Drive: ${errorText(error)}`);
|
|
1406
1598
|
}
|
|
1407
1599
|
}
|
|
1408
1600
|
|
|
1409
1601
|
for (const el of document.querySelectorAll(".if-drive-available")) el.style.display = "none";
|
|
1410
1602
|
(async () => {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1603
|
+
try {
|
|
1604
|
+
const available = await googleDrive.initialise();
|
|
1605
|
+
if (available) {
|
|
1606
|
+
for (const el of document.querySelectorAll(".if-drive-available")) el.style.display = "";
|
|
1607
|
+
await gdAuth(true);
|
|
1608
|
+
}
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
console.log(`Google Drive is unavailable: ${errorText(error)}`);
|
|
1415
1611
|
}
|
|
1416
1612
|
})();
|
|
1417
1613
|
const googleDriveModal = new bootstrap.Modal(googleDriveEl);
|
|
@@ -1440,8 +1636,8 @@ googleDriveEl.addEventListener("show.bs.modal", async function () {
|
|
|
1440
1636
|
utils.noteEvent("google-drive", "click", item.name);
|
|
1441
1637
|
setDisc1Image(`gd:${item.id}/${item.name}`);
|
|
1442
1638
|
googleDriveModal.hide();
|
|
1443
|
-
const ssd = await gdLoad(item);
|
|
1444
|
-
if (ssd)
|
|
1639
|
+
const ssd = await gdLoad(item, layoutForDrive(0));
|
|
1640
|
+
if (ssd) putDiscIn(0, ssd);
|
|
1445
1641
|
});
|
|
1446
1642
|
}
|
|
1447
1643
|
});
|
|
@@ -1457,7 +1653,7 @@ for (const image of availableImages) {
|
|
|
1457
1653
|
utils.noteEvent("images", "click", image.file);
|
|
1458
1654
|
setDisc1Image(image.file);
|
|
1459
1655
|
$discsModal.hide();
|
|
1460
|
-
|
|
1656
|
+
putDiscIn(0, await loadDiscImage(parsedQuery.disc1, layoutForDrive(0)));
|
|
1461
1657
|
});
|
|
1462
1658
|
}
|
|
1463
1659
|
|
|
@@ -1476,7 +1672,7 @@ document.querySelector("#google-drive form").addEventListener("submit", async fu
|
|
|
1476
1672
|
try {
|
|
1477
1673
|
data = discType.saver(processor.fdc.drives[0].disc);
|
|
1478
1674
|
} catch (e) {
|
|
1479
|
-
loadingFinished(`
|
|
1675
|
+
loadingFinished(`Unable to create ${name} on Google Drive: ${errorText(e)}`);
|
|
1480
1676
|
return;
|
|
1481
1677
|
}
|
|
1482
1678
|
name = replaceOrAddExtension(name, discType.extension);
|
|
@@ -1497,11 +1693,11 @@ document.querySelector("#google-drive form").addEventListener("submit", async fu
|
|
|
1497
1693
|
try {
|
|
1498
1694
|
const result = await googleDrive.create(processor.fdc, name, data);
|
|
1499
1695
|
setDisc1Image("gd:" + result.fileId + "/" + name);
|
|
1500
|
-
|
|
1696
|
+
putDiscIn(0, result.disc);
|
|
1501
1697
|
loadingFinished();
|
|
1502
1698
|
} catch (error) {
|
|
1503
1699
|
console.error(`Error creating Google Drive disc: ${error}`, error);
|
|
1504
|
-
loadingFinished(`
|
|
1700
|
+
loadingFinished(`Unable to create ${name} on Google Drive: ${errorText(error)}`);
|
|
1505
1701
|
}
|
|
1506
1702
|
});
|
|
1507
1703
|
|
|
@@ -1567,6 +1763,9 @@ document.getElementById("save-state").addEventListener("click", async function (
|
|
|
1567
1763
|
const discKey = driveIndex === 0 ? "disc1" : "disc2";
|
|
1568
1764
|
const crcKey = discKey + "Crc32";
|
|
1569
1765
|
media[crcKey] = driveDisc.originalImageCrc32;
|
|
1766
|
+
// The snapshot's dirty tracks are indexed by physical track, so restoring has to lay
|
|
1767
|
+
// the disc out the way this one was rather than work it out again.
|
|
1768
|
+
media[discKey + "Layout"] = driveDisc.is40Track ? DiscLayout.expanded40 : DiscLayout.contiguous;
|
|
1570
1769
|
if (!media[discKey] && driveDisc.originalImageData) {
|
|
1571
1770
|
media[discKey + "ImageData"] = driveDisc.originalImageData;
|
|
1572
1771
|
media[discKey + "Name"] = driveDisc.name;
|
|
@@ -1742,90 +1941,85 @@ const startPromise = (async () => {
|
|
|
1742
1941
|
// Ideally would start the loads first. But their completion needs the FDC from the processor
|
|
1743
1942
|
const imageLoads = [];
|
|
1744
1943
|
|
|
1944
|
+
function startImageLoad(description, load) {
|
|
1945
|
+
const loading = (async () => {
|
|
1946
|
+
try {
|
|
1947
|
+
await load();
|
|
1948
|
+
} catch (error) {
|
|
1949
|
+
console.error(`Error loading ${description}:`, error);
|
|
1950
|
+
toast(`Could not load ${description}: ${error?.message ?? error}`, { title: "Loading" });
|
|
1951
|
+
}
|
|
1952
|
+
})();
|
|
1953
|
+
imageLoads.push(loading);
|
|
1954
|
+
return loading;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1745
1957
|
if (discImage) {
|
|
1746
|
-
|
|
1747
|
-
(
|
|
1748
|
-
const disc = await loadDiscImage(discImage);
|
|
1749
|
-
processor.fdc.loadDisc(0, disc);
|
|
1750
|
-
})(),
|
|
1958
|
+
startImageLoad(`disc ${discImage}`, async () =>
|
|
1959
|
+
putDiscIn(0, await loadDiscImage(discImage, layoutForDrive(0))),
|
|
1751
1960
|
);
|
|
1752
1961
|
}
|
|
1753
1962
|
|
|
1754
1963
|
if (secondDiscImage) {
|
|
1755
|
-
|
|
1756
|
-
(
|
|
1757
|
-
const disc = await loadDiscImage(secondDiscImage);
|
|
1758
|
-
processor.fdc.loadDisc(1, disc);
|
|
1759
|
-
})(),
|
|
1964
|
+
startImageLoad(`disc ${secondDiscImage}`, async () =>
|
|
1965
|
+
putDiscIn(1, await loadDiscImage(secondDiscImage, layoutForDrive(1))),
|
|
1760
1966
|
);
|
|
1761
1967
|
}
|
|
1762
1968
|
|
|
1763
1969
|
if (parsedQuery.tape) {
|
|
1764
|
-
|
|
1765
|
-
(async () => {
|
|
1766
|
-
const tape = await loadTapeImage(parsedQuery.tape);
|
|
1767
|
-
setProcessorTape(tape);
|
|
1768
|
-
})(),
|
|
1769
|
-
);
|
|
1970
|
+
startImageLoad(`tape ${parsedQuery.tape}`, async () => setProcessorTape(await loadTapeImage(parsedQuery.tape)));
|
|
1770
1971
|
}
|
|
1771
1972
|
|
|
1772
1973
|
if (mmcImage && model.isAtom) {
|
|
1773
|
-
|
|
1774
|
-
(async () => {
|
|
1775
|
-
const files = await LoadSD(mmcImage);
|
|
1776
|
-
processor.atommc.SetMMCData(files);
|
|
1777
|
-
})(),
|
|
1778
|
-
);
|
|
1974
|
+
startImageLoad(`MMC image ${mmcImage}`, async () => processor.atommc.SetMMCData(await LoadSD(mmcImage)));
|
|
1779
1975
|
}
|
|
1780
1976
|
|
|
1781
1977
|
async function insertBasic(getBasicPromise, needsRun) {
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
});
|
|
1807
|
-
return tokenised; // Explicitly return the result
|
|
1808
|
-
})();
|
|
1809
|
-
|
|
1810
|
-
imageLoads.push(basicLoadPromise);
|
|
1811
|
-
return basicLoadPromise; // Return promise for caller to await if needed
|
|
1978
|
+
const prog = await getBasicPromise;
|
|
1979
|
+
const t = await tokeniser.create();
|
|
1980
|
+
const tokenised = await t.tokenise(prog);
|
|
1981
|
+
|
|
1982
|
+
const idleAddr = processor.model.isMaster ? 0xe7e6 : 0xe581;
|
|
1983
|
+
const hook = processor.debugInstruction.add(function (addr) {
|
|
1984
|
+
if (addr !== idleAddr) return;
|
|
1985
|
+
const page = processor.readmem(0x18) << 8;
|
|
1986
|
+
for (let i = 0; i < tokenised.length; ++i) {
|
|
1987
|
+
processor.writemem(page + i, tokenised.charCodeAt(i));
|
|
1988
|
+
}
|
|
1989
|
+
// Set VARTOP (0x12/3) and TOP(0x02/3)
|
|
1990
|
+
const end = page + tokenised.length;
|
|
1991
|
+
const endLow = end & 0xff;
|
|
1992
|
+
const endHigh = (end >>> 8) & 0xff;
|
|
1993
|
+
processor.writemem(0x02, endLow);
|
|
1994
|
+
processor.writemem(0x03, endHigh);
|
|
1995
|
+
processor.writemem(0x12, endLow);
|
|
1996
|
+
processor.writemem(0x13, endHigh);
|
|
1997
|
+
hook.remove();
|
|
1998
|
+
if (needsRun) {
|
|
1999
|
+
autoRunBasic();
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
1812
2002
|
}
|
|
1813
2003
|
|
|
1814
2004
|
if (parsedQuery.loadBasic) {
|
|
1815
2005
|
const needsRun = needsAutoboot === "run";
|
|
1816
2006
|
needsAutoboot = "";
|
|
1817
2007
|
|
|
1818
|
-
await
|
|
1819
|
-
(
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2008
|
+
await startImageLoad(`BASIC program ${parsedQuery.loadBasic}`, () =>
|
|
2009
|
+
insertBasic(
|
|
2010
|
+
(async () => {
|
|
2011
|
+
const data = await utils.loadData(parsedQuery.loadBasic);
|
|
2012
|
+
return String.fromCharCode.apply(null, data);
|
|
2013
|
+
})(),
|
|
2014
|
+
needsRun,
|
|
2015
|
+
),
|
|
1824
2016
|
);
|
|
1825
2017
|
}
|
|
1826
2018
|
|
|
1827
2019
|
if (parsedQuery.embedBasic) {
|
|
1828
|
-
await
|
|
2020
|
+
await startImageLoad("the BASIC program from the URL", () =>
|
|
2021
|
+
insertBasic(Promise.resolve(parsedQuery.embedBasic), true),
|
|
2022
|
+
);
|
|
1829
2023
|
}
|
|
1830
2024
|
|
|
1831
2025
|
return Promise.all(imageLoads);
|
|
@@ -1837,7 +2031,7 @@ const startPromise = (async () => {
|
|
|
1837
2031
|
|
|
1838
2032
|
switch (needsAutoboot) {
|
|
1839
2033
|
case "boot":
|
|
1840
|
-
|
|
2034
|
+
showAutoboot(true);
|
|
1841
2035
|
autoboot(discImage);
|
|
1842
2036
|
break;
|
|
1843
2037
|
case "type":
|
|
@@ -1850,7 +2044,7 @@ const startPromise = (async () => {
|
|
|
1850
2044
|
autoRunTape();
|
|
1851
2045
|
break;
|
|
1852
2046
|
default:
|
|
1853
|
-
|
|
2047
|
+
showAutoboot(false);
|
|
1854
2048
|
break;
|
|
1855
2049
|
}
|
|
1856
2050
|
|
|
@@ -1998,6 +2192,23 @@ rewindUI.updateButtonState();
|
|
|
1998
2192
|
if (processor.fdc) new DiscVisualiser({ fdc: processor.fdc });
|
|
1999
2193
|
else document.getElementById("disc-visualiser-open").classList.add("disabled");
|
|
2000
2194
|
|
|
2195
|
+
for (const item of document.querySelectorAll(".drive-tracks")) {
|
|
2196
|
+
const driveIndex = Number(item.dataset.drive);
|
|
2197
|
+
const drive = processor.fdc?.drives[driveIndex];
|
|
2198
|
+
const fixed = drive ? tracksPerStepForDrive(driveIndex) : undefined;
|
|
2199
|
+
if (fixed !== undefined) drive.tracksPerStep = fixed;
|
|
2200
|
+
for (const button of driveTracksButtons(driveIndex)) {
|
|
2201
|
+
button.disabled = !drive;
|
|
2202
|
+
button.addEventListener("click", (event) => {
|
|
2203
|
+
// Setting a switch is not picking from a menu, so leave the menu where it is.
|
|
2204
|
+
event.stopPropagation();
|
|
2205
|
+
drive.tracksPerStep = tracksPerStepFor(button.dataset.tracks);
|
|
2206
|
+
showDriveTracks(driveIndex);
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
if (drive) showDriveTracks(driveIndex);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2001
2212
|
function draw(now) {
|
|
2002
2213
|
if (!running) {
|
|
2003
2214
|
last = 0;
|