jsbeeb 1.22.4 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +16 -11
- package/src/6502.js +39 -42
- package/src/6502.opcodes.js +198 -123
- 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 +7 -23
- 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/jsbeeb.css +33 -0
- 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 +69 -184
- package/src/media-resolver.js +68 -0
- package/src/mmc.js +3 -2
- package/src/models.js +40 -4
- 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 +307 -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 +12 -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 +42 -72
- package/src/web/console-surface.js +25 -0
- package/src/web/debug.js +8 -49
- 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/icons.js +42 -0
- package/src/web/keyboard-setup.js +29 -40
- 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
package/src/main.js
CHANGED
|
@@ -3,12 +3,11 @@ import { version } from "../package.json";
|
|
|
3
3
|
import "bootswatch/dist/darkly/bootstrap.min.css";
|
|
4
4
|
import "./jsbeeb.css";
|
|
5
5
|
|
|
6
|
-
import * as utils from "./utils.js";
|
|
7
6
|
import { Debugger } from "./web/debug.js";
|
|
8
|
-
import
|
|
9
|
-
import { GamePad } from "./gamepads.js";
|
|
7
|
+
import { GamePad } from "./web/gamepads.js";
|
|
10
8
|
import { initialise as electron } from "./app/electron.js";
|
|
11
9
|
import { AudioHandler } from "./web/audio-handler.js";
|
|
10
|
+
import { installIcons } from "./web/icons.js";
|
|
12
11
|
import { QuickSettings } from "./web/quick-settings.js";
|
|
13
12
|
import { toast } from "./web/toast.js";
|
|
14
13
|
import { BuiltInImages, MediaLoader } from "./web/media-loader.js";
|
|
@@ -20,7 +19,9 @@ import { isSnapshotFile, SnapshotUI } from "./web/snapshot-ui.js";
|
|
|
20
19
|
import { Autoboot } from "./web/autoboot.js";
|
|
21
20
|
import { Display } from "./web/display.js";
|
|
22
21
|
import { Layout } from "./web/layout.js";
|
|
23
|
-
import { EmulationLoop
|
|
22
|
+
import { EmulationLoop } from "./web/emulation-loop.js";
|
|
23
|
+
import { RunControls } from "./web/run-controls.js";
|
|
24
|
+
import { exposeConsoleSurface } from "./web/console-surface.js";
|
|
24
25
|
import { KeyboardSetup } from "./web/keyboard-setup.js";
|
|
25
26
|
import { AccessibilitySwitches } from "./web/accessibility-switches.js";
|
|
26
27
|
import { AnalogueInputs } from "./web/analogue-inputs.js";
|
|
@@ -30,12 +31,17 @@ import { Drives } from "./web/drives.js";
|
|
|
30
31
|
import { UrlState } from "./web/url-state.js";
|
|
31
32
|
import { Modals } from "./web/modals.js";
|
|
32
33
|
import { Settings } from "./web/settings.js";
|
|
34
|
+
import { Config } from "./web/config.js";
|
|
35
|
+
import { SpeechOutput } from "./web/speech-output.js";
|
|
33
36
|
import { Printer } from "./printer.js";
|
|
34
|
-
import { RewindBuffer } from "./rewind.js";
|
|
35
37
|
import { RewindUI } from "./web/rewind-ui.js";
|
|
36
38
|
import { DiscVisualiser } from "./web/disc-visualiser.js";
|
|
37
|
-
import {
|
|
39
|
+
import { PageActions } from "./web/page-actions.js";
|
|
38
40
|
import { parseMediaParams, processAutobootParams, processDriveTrackParams, processInputParams } from "./url-params.js";
|
|
41
|
+
import { adaptKeyCodesToBrowser, keyCodes, userKeymap } from "./keymap.js";
|
|
42
|
+
|
|
43
|
+
installIcons();
|
|
44
|
+
adaptKeyCodesToBrowser();
|
|
39
45
|
|
|
40
46
|
// ------------------------------------------------------------------------
|
|
41
47
|
// What the URL asked for.
|
|
@@ -67,11 +73,6 @@ const stationId = parsedQuery.stationId !== undefined ? parsedQuery.stationId :
|
|
|
67
73
|
const tryGl = parsedQuery.glEnabled ?? true;
|
|
68
74
|
const lowLatency = parsedQuery.lowLatency ?? true;
|
|
69
75
|
|
|
70
|
-
if (parsedQuery.embed) {
|
|
71
|
-
for (const el of document.querySelectorAll(".embed-hide")) el.style.display = "none";
|
|
72
|
-
document.body.style.backgroundColor = "transparent";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
76
|
// ------------------------------------------------------------------------
|
|
76
77
|
// The pieces that exist before the machine: settings, screen, sound.
|
|
77
78
|
// ------------------------------------------------------------------------
|
|
@@ -85,29 +86,26 @@ if (!window.isSecureContext)
|
|
|
85
86
|
quietKey: "quietInsecureGamepads",
|
|
86
87
|
});
|
|
87
88
|
|
|
88
|
-
const printer = new Printer(
|
|
89
|
-
onOutput: (char) => frontPanel.printChar(char),
|
|
90
|
-
onFirstOutput: () =>
|
|
91
|
-
toast("Printer output is being kept. Press Ctrl-B to open the printer window.", {
|
|
92
|
-
title: "Printer",
|
|
93
|
-
quietKey: "quietPrinterOutput",
|
|
94
|
-
}),
|
|
95
|
-
});
|
|
89
|
+
const printer = new Printer();
|
|
96
90
|
|
|
97
91
|
const accessibilitySwitches = new AccessibilitySwitches();
|
|
98
92
|
|
|
99
93
|
const settings = new Settings({ urlState });
|
|
100
|
-
const {
|
|
94
|
+
const { keyLayout, displayMode, audioOutput, speakerAmount } = settings;
|
|
101
95
|
const model = settings.model;
|
|
96
|
+
const config = new Config(settings);
|
|
97
|
+
|
|
98
|
+
const speechOutput = new SpeechOutput();
|
|
99
|
+
const speak = (enabled) => {
|
|
100
|
+
speechOutput.enabled = enabled;
|
|
101
|
+
if (enabled && typeof speechSynthesis === "undefined")
|
|
102
|
+
toast("This browser has no speech synthesis, so speech output has nothing to speak with.", { title: "Speech" });
|
|
103
|
+
};
|
|
104
|
+
speak(settings.speechOutput);
|
|
105
|
+
settings.on("speechOutput", speak);
|
|
102
106
|
|
|
103
107
|
// Must come after we know the model, to validate names against those of the hardware.
|
|
104
|
-
const keyMappingWarnings = processInputParams(
|
|
105
|
-
parsedQuery,
|
|
106
|
-
model.isAtom ? utils_atom.ATOM : utils.BBC,
|
|
107
|
-
utils.keyCodes,
|
|
108
|
-
utils.userKeymap,
|
|
109
|
-
gamepad,
|
|
110
|
-
);
|
|
108
|
+
const keyMappingWarnings = processInputParams(parsedQuery, model.keys, keyCodes, userKeymap, gamepad);
|
|
111
109
|
if (keyMappingWarnings.length) {
|
|
112
110
|
toast(`${keyMappingWarnings.join(" ")} The key names are listed in the README.`, {
|
|
113
111
|
title: "Mappings in the URL",
|
|
@@ -149,21 +147,14 @@ const audioHandler = new AudioHandler({
|
|
|
149
147
|
noSeek,
|
|
150
148
|
cpuSpeed,
|
|
151
149
|
isAtom: model.isAtom,
|
|
152
|
-
hasMusic5000:
|
|
150
|
+
hasMusic5000: settings.hasMusic5000,
|
|
153
151
|
});
|
|
154
152
|
// Firefox will report that audio is suspended even when it will
|
|
155
153
|
// start playing without user interaction, so we need to delay a
|
|
156
154
|
// little to get a reliable indication.
|
|
157
155
|
window.setTimeout(() => audioHandler.checkStatus(), 1000);
|
|
158
156
|
|
|
159
|
-
|
|
160
|
-
{
|
|
161
|
-
onAudioOutput: (output) => settings.applyAudioOutput(output),
|
|
162
|
-
onSpeakerAmount: (amount) => settings.applySpeakerAmount(amount),
|
|
163
|
-
onDisplayMode: (mode) => settings.applyDisplayMode(mode),
|
|
164
|
-
},
|
|
165
|
-
{ audioOutput, speakerAmount, displayMode },
|
|
166
|
-
);
|
|
157
|
+
new QuickSettings(settings);
|
|
167
158
|
|
|
168
159
|
// ------------------------------------------------------------------------
|
|
169
160
|
// The machine, the loop that runs it, and everything that feeds it.
|
|
@@ -172,7 +163,7 @@ const quickSettings = new QuickSettings(
|
|
|
172
163
|
// Depends on the settings having applied the URL parameters.
|
|
173
164
|
const machine = new Machine({
|
|
174
165
|
model,
|
|
175
|
-
|
|
166
|
+
settings,
|
|
176
167
|
parsedQuery,
|
|
177
168
|
keyLayout,
|
|
178
169
|
cpuMultiplier,
|
|
@@ -187,7 +178,7 @@ const machine = new Machine({
|
|
|
187
178
|
});
|
|
188
179
|
const processor = machine.processor;
|
|
189
180
|
|
|
190
|
-
const
|
|
181
|
+
const { keyboard } = new KeyboardSetup({
|
|
191
182
|
actions: {
|
|
192
183
|
enterDebugger: () => loop.stop(true),
|
|
193
184
|
reload: () => window.location.reload(),
|
|
@@ -196,6 +187,7 @@ const keys = new KeyboardSetup({
|
|
|
196
187
|
openPrinter: () => frontPanel.checkPrinterWindow(),
|
|
197
188
|
pause: () => loop.stop(false),
|
|
198
189
|
resume: () => loop.go(),
|
|
190
|
+
paste: (text) => keyboard.sendRawKeyboard(autoBoot.stringToMachineKeys(text), true),
|
|
199
191
|
onAnyKeyDown: () => {
|
|
200
192
|
audioHandler.tryResume();
|
|
201
193
|
inputs.ensureMicrophoneRunning();
|
|
@@ -207,31 +199,20 @@ const keys = new KeyboardSetup({
|
|
|
207
199
|
keyLayout,
|
|
208
200
|
});
|
|
209
201
|
|
|
210
|
-
const rewindBuffer = new RewindBuffer(30);
|
|
211
202
|
const loop = new EmulationLoop({
|
|
212
203
|
processor,
|
|
213
204
|
display,
|
|
214
205
|
audioHandler,
|
|
215
206
|
dbgr,
|
|
216
207
|
gamepad,
|
|
217
|
-
keyboard
|
|
218
|
-
syncLights: () => frontPanel.syncLights(),
|
|
219
|
-
rewindBuffer,
|
|
220
|
-
onRewindCaptured: () => rewindUI.updateButtonState(),
|
|
208
|
+
keyboard,
|
|
221
209
|
clocksPerSecond,
|
|
222
210
|
cpuSpeed,
|
|
223
211
|
fastTape: !!parsedQuery.fasttape,
|
|
224
212
|
audioStatsNode,
|
|
225
213
|
});
|
|
226
214
|
|
|
227
|
-
const
|
|
228
|
-
const debugPlay = document.getElementById("debug-play");
|
|
229
|
-
loop.addEventListener("running", () => {
|
|
230
|
-
const running = loop.isRunning();
|
|
231
|
-
keys.setRunning(running);
|
|
232
|
-
debugPlay.disabled = running;
|
|
233
|
-
debugPause.disabled = !running;
|
|
234
|
-
});
|
|
215
|
+
const runControls = new RunControls({ loop, dbgr, keyboard });
|
|
235
216
|
|
|
236
217
|
const modals = new Modals({ loop });
|
|
237
218
|
|
|
@@ -248,7 +229,7 @@ const media = new MediaLoader({
|
|
|
248
229
|
const autoBoot = new Autoboot({
|
|
249
230
|
model,
|
|
250
231
|
processor,
|
|
251
|
-
sendKeys: (keysToSend, check) =>
|
|
232
|
+
sendKeys: (keysToSend, check) => keyboard.sendRawKeyboard(keysToSend, check),
|
|
252
233
|
});
|
|
253
234
|
const autobootTicks = new AutobootTicks({ urlState });
|
|
254
235
|
const sthPicker = new SthPicker({
|
|
@@ -283,12 +264,11 @@ const snapshots = new SnapshotUI({
|
|
|
283
264
|
const inputs = new AnalogueInputs({
|
|
284
265
|
processor,
|
|
285
266
|
screenCanvas,
|
|
286
|
-
getGamepads: machine.
|
|
287
|
-
|
|
288
|
-
config,
|
|
267
|
+
getGamepads: machine.spec.getGamepads,
|
|
268
|
+
settings,
|
|
289
269
|
audioHandler,
|
|
290
270
|
});
|
|
291
|
-
if (
|
|
271
|
+
if (settings.microphoneChannel !== undefined) {
|
|
292
272
|
// We need to use setTimeout to make sure this runs after the page has loaded
|
|
293
273
|
// This is needed because some browsers require user interaction for audio context
|
|
294
274
|
setTimeout(async () => {
|
|
@@ -296,110 +276,54 @@ if (parsedQuery.microphoneChannel !== undefined) {
|
|
|
296
276
|
}, 1000);
|
|
297
277
|
}
|
|
298
278
|
// Apply ADC source settings from URL parameters
|
|
299
|
-
inputs.updateAdcSources(
|
|
279
|
+
inputs.updateAdcSources(settings.mouseJoystickEnabled, settings.microphoneChannel);
|
|
300
280
|
|
|
301
|
-
const frontPanel = new FrontPanel({ processor, model, printer });
|
|
281
|
+
const frontPanel = new FrontPanel({ processor, model, printer, loop });
|
|
302
282
|
|
|
303
283
|
// ------------------------------------------------------------------------
|
|
304
284
|
// Rewind, the visualiser and the layout.
|
|
305
285
|
// ------------------------------------------------------------------------
|
|
306
286
|
|
|
307
|
-
const rewindUI = new RewindUI({
|
|
308
|
-
rewindBuffer,
|
|
309
|
-
processor,
|
|
310
|
-
video,
|
|
311
|
-
captureInterval: RewindCaptureInterval,
|
|
312
|
-
loop,
|
|
313
|
-
});
|
|
287
|
+
const rewindUI = new RewindUI({ processor, video, loop });
|
|
314
288
|
rewindUI.updateButtonState();
|
|
315
289
|
|
|
316
|
-
|
|
317
|
-
else document.getElementById("disc-visualiser-open").classList.add("disabled");
|
|
290
|
+
new DiscVisualiser({ fdc: processor.fdc });
|
|
318
291
|
|
|
319
292
|
const layout = new Layout({
|
|
320
293
|
screenCanvas,
|
|
321
294
|
display,
|
|
322
|
-
embed: parsedQuery.embed
|
|
295
|
+
embed: !!parsedQuery.embed,
|
|
323
296
|
sidebars: { left: parsedQuery.sbLeft, right: parsedQuery.sbRight, bottom: parsedQuery.sbBottom },
|
|
324
297
|
});
|
|
325
298
|
|
|
326
299
|
// Everything a setting can reach now exists.
|
|
327
|
-
settings.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
for (const el of document.querySelectorAll(".initially-hidden")) el.classList.remove("initially-hidden");
|
|
334
|
-
|
|
335
|
-
const pastetext = document.getElementById("paste-text");
|
|
336
|
-
pastetext.closest("form").addEventListener("submit", (event) => event.preventDefault());
|
|
337
|
-
pastetext.addEventListener("paste", function (event) {
|
|
338
|
-
const text = event.clipboardData.getData("text/plain");
|
|
339
|
-
keys.sendRawKeyboard(autoBoot.stringToMachineKeys(text), true);
|
|
300
|
+
settings.on("audioOutput", (output) => audioHandler.setAudioOutput(output));
|
|
301
|
+
settings.on("speakerAmount", (amount) => audioHandler.setSpeakerAmount(amount));
|
|
302
|
+
settings.on("displayMode", (mode) => {
|
|
303
|
+
display.setMode(mode);
|
|
304
|
+
// The monitor picture may have changed shape.
|
|
305
|
+
layout.resize();
|
|
340
306
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
loop.setEmulationLead(audioHandler.setWindowFocused(false));
|
|
307
|
+
settings.on("keyLayout", (chosen) => keyboard.setKeyLayout(chosen));
|
|
308
|
+
settings.on("tubeCpuMultiplier", (multiplier) => {
|
|
309
|
+
if (processor.hasTube) processor.tube.cpuMultiplier = multiplier;
|
|
345
310
|
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
function resumeFromDebugger() {
|
|
353
|
-
dbgr.hide();
|
|
354
|
-
keys.resumeEmulation();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
debugPause.addEventListener("click", pauseIntoDebugger);
|
|
358
|
-
debugPlay.addEventListener("click", resumeFromDebugger);
|
|
359
|
-
|
|
360
|
-
// To lower chance of data loss, only accept drop events in the drop
|
|
361
|
-
// zone in the menu bar.
|
|
362
|
-
document.addEventListener("dragover", function (event) {
|
|
363
|
-
event.preventDefault();
|
|
364
|
-
event.dataTransfer.dropEffect = "none";
|
|
365
|
-
});
|
|
366
|
-
document.addEventListener("drop", function (event) {
|
|
367
|
-
event.preventDefault();
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
window.addEventListener("beforeunload", function (event) {
|
|
371
|
-
if (loop.isRunning() && processor.sysvia.hasAnyKeyDown()) {
|
|
372
|
-
const message =
|
|
373
|
-
"It seems like you're still using the emulator. If you're in Chrome, it's impossible for jsbeeb to prevent some shortcuts (like ctrl-W) from performing their default behaviour (e.g. closing the window).\n" +
|
|
374
|
-
"As a workarond, create an 'Application Shortcut' from the Tools menu. When jsbeeb runs as an application, it *can* prevent ctrl-W from closing the window.";
|
|
375
|
-
event.preventDefault();
|
|
376
|
-
event.returnValue = message;
|
|
377
|
-
return message;
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
function hardReset() {
|
|
382
|
-
rewindUI.reset();
|
|
383
|
-
processor.reset(true);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
document.getElementById("hard-reset").addEventListener("click", function (event) {
|
|
387
|
-
hardReset();
|
|
388
|
-
event.preventDefault();
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
document.getElementById("soft-reset").addEventListener("click", function (event) {
|
|
392
|
-
processor.reset(false);
|
|
393
|
-
event.preventDefault();
|
|
311
|
+
const rerouteAnalogue = () => inputs.updateAdcSources(settings.mouseJoystickEnabled, settings.microphoneChannel);
|
|
312
|
+
settings.on("mouseJoystickEnabled", rerouteAnalogue);
|
|
313
|
+
settings.on("microphoneChannel", (channel) => {
|
|
314
|
+
rerouteAnalogue();
|
|
315
|
+
if (channel !== undefined) inputs.setupMicrophone();
|
|
394
316
|
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
317
|
+
config.addEventListener("restart-required", async () => {
|
|
318
|
+
const restart = await modals.confirm(
|
|
319
|
+
"Your change is saved, but only takes effect when the emulator restarts. Restart now?",
|
|
320
|
+
"Restart now",
|
|
321
|
+
"Later",
|
|
322
|
+
);
|
|
323
|
+
if (restart) window.location.reload();
|
|
399
324
|
});
|
|
400
325
|
|
|
401
|
-
|
|
402
|
-
if (Object.hasOwn(parsedQuery, "pp-tos")) modals.show("pp-tos");
|
|
326
|
+
const page = new PageActions({ loop, processor, keyboard, audioHandler, rewindUI, modals, parsedQuery, version });
|
|
403
327
|
|
|
404
328
|
// ------------------------------------------------------------------------
|
|
405
329
|
// Start it up.
|
|
@@ -461,47 +385,14 @@ const startPromise = machine.start({
|
|
|
461
385
|
// The console surface the wiki documents, and the desktop app's hooks.
|
|
462
386
|
// ------------------------------------------------------------------------
|
|
463
387
|
|
|
464
|
-
|
|
465
|
-
// safely run from the JS console in firefox.
|
|
466
|
-
window.benchmarkCpu = utils.debounce((numCycles) => loop.benchmarkCpu(numCycles), 1);
|
|
467
|
-
window.profileCpu = utils.debounce((arg) => loop.profileCpu(arg), 1);
|
|
468
|
-
window.benchmarkVideo = utils.debounce((numCycles) => loop.benchmarkVideo(numCycles), 1);
|
|
469
|
-
window.profileVideo = utils.debounce((arg) => loop.profileVideo(arg), 1);
|
|
470
|
-
window.go = () => loop.go();
|
|
471
|
-
window.stop = (debug) => loop.stop(debug);
|
|
472
|
-
window.soundChip = audioHandler.soundChip;
|
|
473
|
-
window.processor = processor;
|
|
474
|
-
window.video = video;
|
|
475
|
-
window.hd = function (start, end) {
|
|
476
|
-
console.log(
|
|
477
|
-
utils.hd(
|
|
478
|
-
function (x) {
|
|
479
|
-
return processor.readmem(x);
|
|
480
|
-
},
|
|
481
|
-
start,
|
|
482
|
-
end,
|
|
483
|
-
),
|
|
484
|
-
);
|
|
485
|
-
};
|
|
486
|
-
window.m7dump = function () {
|
|
487
|
-
console.log(
|
|
488
|
-
utils.hd(
|
|
489
|
-
function (x) {
|
|
490
|
-
return processor.readmem(x) & 0x7f;
|
|
491
|
-
},
|
|
492
|
-
0x7c00,
|
|
493
|
-
0x7fe8,
|
|
494
|
-
{ width: 40, gap: false },
|
|
495
|
-
),
|
|
496
|
-
);
|
|
497
|
-
};
|
|
388
|
+
exposeConsoleSurface(window, { loop, processor, video, audioHandler });
|
|
498
389
|
|
|
499
390
|
// Hooks for electron.
|
|
500
391
|
electron({
|
|
501
392
|
loadDiscImage: media.loadDiscImage.bind(media),
|
|
502
393
|
loadTapeImage: media.loadTapeImage.bind(media),
|
|
503
394
|
processor,
|
|
504
|
-
|
|
395
|
+
settings,
|
|
505
396
|
media,
|
|
506
397
|
modals: {
|
|
507
398
|
show: (modalId, sthType) => {
|
|
@@ -514,17 +405,11 @@ electron({
|
|
|
514
405
|
},
|
|
515
406
|
loadStateFile: snapshots.loadStateFromFile.bind(snapshots),
|
|
516
407
|
actions: {
|
|
517
|
-
"soft-reset": () =>
|
|
518
|
-
"hard-reset": hardReset,
|
|
408
|
+
"soft-reset": () => page.softReset(),
|
|
409
|
+
"hard-reset": () => page.hardReset(),
|
|
519
410
|
"save-state": () => snapshots.saveState(),
|
|
520
411
|
rewind: () => rewindUI.open(),
|
|
521
|
-
pause:
|
|
522
|
-
resume:
|
|
412
|
+
pause: () => runControls.pause(),
|
|
413
|
+
resume: () => runControls.resume(),
|
|
523
414
|
},
|
|
524
415
|
});
|
|
525
|
-
|
|
526
|
-
// Display version in About dialog
|
|
527
|
-
const versionElement = document.getElementById("jsbeeb-version");
|
|
528
|
-
if (versionElement) {
|
|
529
|
-
versionElement.textContent = `Version ${version}`;
|
|
530
|
-
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { unzipDiscImage } from "./archive.js";
|
|
2
|
+
import { stringToUint8Array } from "./binary.js";
|
|
3
|
+
import { loadData } from "./loader.js";
|
|
4
|
+
|
|
5
|
+
/** Splits an image reference into its schema (empty for a bare name) and the image it names. */
|
|
6
|
+
export function splitImage(image) {
|
|
7
|
+
const match = image.match(/(([^:]+):\/?\/?|[!^|])?(.*)/);
|
|
8
|
+
return { schema: match[2] || match[1] || "", image: match[3] };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Where a bare name is looked for, and which registered source serves the archive.
|
|
12
|
+
const Kinds = {
|
|
13
|
+
disc: { folder: "discs", sth: "sth" },
|
|
14
|
+
tape: { folder: "tapes", sth: "tapeSth" },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** The image itself when `name` is a zip, else the bytes as given, either way as `{ name, data, ignored }`. */
|
|
18
|
+
export function openIfZip(name, data) {
|
|
19
|
+
return /\.zip/i.test(name) ? unzipDiscImage(data) : { name, data, ignored: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Turns any image reference the URL can name into bytes, `{ name, data, ignored }`,
|
|
24
|
+
* for a disc or a tape: the archives through the sources registered for them,
|
|
25
|
+
* `data:` and `b64data:` inline, `http:`, `https:` and `file:` by URL, and a
|
|
26
|
+
* bare name from the built-in folder. Zips are opened once, here. Nothing in
|
|
27
|
+
* it needs a page, so a headless machine can load the same references.
|
|
28
|
+
*/
|
|
29
|
+
export class MediaResolver {
|
|
30
|
+
constructor({ load = loadData } = {}) {
|
|
31
|
+
this.sources = {};
|
|
32
|
+
this.load = load;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Registers the fetcher behind an archive schema; each archive registers as it is constructed. */
|
|
36
|
+
addSource(schema, fetcher) {
|
|
37
|
+
this.sources[schema] = fetcher;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async resolve(kind, ref) {
|
|
41
|
+
const { schema, image } = splitImage(ref);
|
|
42
|
+
const { folder, sth } = Kinds[kind];
|
|
43
|
+
switch (schema) {
|
|
44
|
+
case "|":
|
|
45
|
+
case "sth":
|
|
46
|
+
return this.source(sth)(image);
|
|
47
|
+
case "hfe":
|
|
48
|
+
return { name: image, data: await this.source("hfe")(image), ignored: [] };
|
|
49
|
+
case "b64data":
|
|
50
|
+
return { name: "disk.ssd", data: stringToUint8Array(atob(image)), ignored: [] };
|
|
51
|
+
case "data":
|
|
52
|
+
return unzipDiscImage(stringToUint8Array(atob(image)));
|
|
53
|
+
case "http":
|
|
54
|
+
case "https":
|
|
55
|
+
case "file":
|
|
56
|
+
// The URL may end in query parameters, which would upset the extension check.
|
|
57
|
+
return openIfZip(new URL(ref).pathname, await this.load(ref));
|
|
58
|
+
default:
|
|
59
|
+
return openIfZip(image, await this.load(`${folder}/${image}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
source(schema) {
|
|
64
|
+
const fetcher = this.sources[schema];
|
|
65
|
+
if (!fetcher) throw new Error(`No ${schema} archive is available here`);
|
|
66
|
+
return fetcher;
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/mmc.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createZipBlob, unzip } from "./archive.js";
|
|
2
|
+
import { loadData } from "./loader.js";
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
highly adapted from:
|
|
@@ -617,7 +618,7 @@ class WFN {
|
|
|
617
618
|
let fname = this.trimToFilename(this.globalData);
|
|
618
619
|
|
|
619
620
|
if (this.filenum === 0) {
|
|
620
|
-
this.fildataIndex = 0; // FIXME
|
|
621
|
+
this.fildataIndex = 0; // FIXME(#1068) should be one for each fildata if going down this line!
|
|
621
622
|
// The scratch file is fixed, so we are backwards compatible with 2.9 firmware
|
|
622
623
|
let fopen = this.f_open(fname, mode);
|
|
623
624
|
if (fopen.error == FR_OK) this.fildata[0] = this.allfiles[fopen.fp];
|
package/src/models.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
1
|
import { NoiseAwareWdFdc } from "./wd-fdc.js";
|
|
4
2
|
import { NoiseAwareIntelFdc } from "./intel-fdc.js";
|
|
5
3
|
import * as opcodes from "./6502.opcodes.js";
|
|
4
|
+
import { AtomCpu6502, Cpu6502 } from "./6502.js";
|
|
5
|
+
import { BBC, stringToBBCKeys } from "./keymap.js";
|
|
6
|
+
import { ATOM, stringToATOMKeys } from "./keymap-atom.js";
|
|
6
7
|
|
|
7
8
|
const CpuModel = Object.freeze({
|
|
8
9
|
MOS6502: 0,
|
|
@@ -19,9 +20,23 @@ const CpuModel = Object.freeze({
|
|
|
19
20
|
* to any number of machines without one session's settings leaking into the next.
|
|
20
21
|
*/
|
|
21
22
|
class Model {
|
|
22
|
-
constructor({
|
|
23
|
+
constructor({
|
|
24
|
+
name,
|
|
25
|
+
shortName = name,
|
|
26
|
+
synonyms,
|
|
27
|
+
os,
|
|
28
|
+
cpuModel,
|
|
29
|
+
isMaster,
|
|
30
|
+
isAtom,
|
|
31
|
+
swram,
|
|
32
|
+
fdc,
|
|
33
|
+
cmosOverride,
|
|
34
|
+
banks,
|
|
35
|
+
clockMhz,
|
|
36
|
+
} = {}) {
|
|
23
37
|
if (!(clockMhz > 0)) throw new Error(`Model ${name} has no clock speed`);
|
|
24
38
|
this.name = name;
|
|
39
|
+
this.shortName = shortName;
|
|
25
40
|
this.synonyms = synonyms;
|
|
26
41
|
this.os = os;
|
|
27
42
|
this.clockMhz = clockMhz;
|
|
@@ -29,6 +44,19 @@ class Model {
|
|
|
29
44
|
this._cpuModel = cpuModel;
|
|
30
45
|
this.isMaster = isMaster;
|
|
31
46
|
this.isAtom = !!isAtom;
|
|
47
|
+
// The one place the two machine families part: everything else asks the model.
|
|
48
|
+
this.Cpu = this.isAtom ? AtomCpu6502 : Cpu6502;
|
|
49
|
+
this.keys = this.isAtom ? ATOM : BBC;
|
|
50
|
+
this.stringToKeys = this.isAtom ? stringToATOMKeys : stringToBBCKeys;
|
|
51
|
+
// The OS write-character vector, watched to capture what the machine prints.
|
|
52
|
+
this.wrchvAddress = this.isAtom ? 0x0208 : 0x020e;
|
|
53
|
+
// Where the machine sits waiting for a key: the Atom kernel's read loop, or BASIC's.
|
|
54
|
+
this.idleAddress = this.isAtom ? 0xfe94 : isMaster ? 0xe7e6 : 0xe581;
|
|
55
|
+
// The Atom ROM polls its keyboard once per VSync, so pasted keys need longer apart.
|
|
56
|
+
this.pasteKeyDelayMs = this.isAtom ? 80 : 50;
|
|
57
|
+
// Two of those 60 Hz scans, 33 ms, with margin: the ROM wants a key seen up
|
|
58
|
+
// twice before it takes the next.
|
|
59
|
+
this.pasteReleaseGapMs = this.isAtom ? 40 : 0;
|
|
32
60
|
this.Fdc = fdc;
|
|
33
61
|
this.swram = swram;
|
|
34
62
|
this.isTest = false;
|
|
@@ -79,6 +107,7 @@ function pickDfs(cmos) {
|
|
|
79
107
|
function atomModel({ name, synonyms, os, banks }) {
|
|
80
108
|
return new Model({
|
|
81
109
|
name,
|
|
110
|
+
shortName: "Atom",
|
|
82
111
|
synonyms,
|
|
83
112
|
os,
|
|
84
113
|
cpuModel: CpuModel.MOS6502,
|
|
@@ -91,7 +120,7 @@ function atomModel({ name, synonyms, os, banks }) {
|
|
|
91
120
|
});
|
|
92
121
|
}
|
|
93
122
|
|
|
94
|
-
// TODO
|
|
123
|
+
// TODO(#1066) semi-bplus-style to get swram for exile hardcoded here
|
|
95
124
|
const beebSwram = [
|
|
96
125
|
true,
|
|
97
126
|
true,
|
|
@@ -132,6 +161,7 @@ const masterSwram = [
|
|
|
132
161
|
export const allModels = [
|
|
133
162
|
new Model({
|
|
134
163
|
name: "BBC B with 8271 (DFS 1.2)",
|
|
164
|
+
shortName: "BBC B",
|
|
135
165
|
synonyms: ["B-DFS1.2", "BBC B with DFS 1.2"],
|
|
136
166
|
os: ["os.rom", "BASIC.ROM", "b/DFS-1.2.rom"],
|
|
137
167
|
cpuModel: CpuModel.MOS6502,
|
|
@@ -142,6 +172,7 @@ export const allModels = [
|
|
|
142
172
|
}),
|
|
143
173
|
new Model({
|
|
144
174
|
name: "BBC B with 8271 (DFS 0.9)",
|
|
175
|
+
shortName: "BBC B",
|
|
145
176
|
synonyms: ["B-DFS0.9", "B", "BBC B with DFS 0.9"],
|
|
146
177
|
os: ["os.rom", "BASIC.ROM", "b/DFS-0.9.rom"],
|
|
147
178
|
cpuModel: CpuModel.MOS6502,
|
|
@@ -152,6 +183,7 @@ export const allModels = [
|
|
|
152
183
|
}),
|
|
153
184
|
new Model({
|
|
154
185
|
name: "BBC B with 1770 (DFS)",
|
|
186
|
+
shortName: "BBC B",
|
|
155
187
|
synonyms: ["B1770"],
|
|
156
188
|
os: ["os.rom", "BASIC.ROM", "b1770/dfs1770.rom", "b1770/zADFS.ROM"],
|
|
157
189
|
cpuModel: CpuModel.MOS6502,
|
|
@@ -163,6 +195,7 @@ export const allModels = [
|
|
|
163
195
|
// putting ADFS in a higher ROM slot gives it priority
|
|
164
196
|
new Model({
|
|
165
197
|
name: "BBC B with 1770 (ADFS)",
|
|
198
|
+
shortName: "BBC B",
|
|
166
199
|
synonyms: ["B1770A"],
|
|
167
200
|
os: ["os.rom", "BASIC.ROM", "b1770/zADFS.ROM", "b1770/dfs1770.rom"],
|
|
168
201
|
cpuModel: CpuModel.MOS6502,
|
|
@@ -173,6 +206,7 @@ export const allModels = [
|
|
|
173
206
|
}),
|
|
174
207
|
new Model({
|
|
175
208
|
name: "BBC Master 128 (DFS)",
|
|
209
|
+
shortName: "Master 128",
|
|
176
210
|
synonyms: ["Master"],
|
|
177
211
|
os: ["master/mos3.20"],
|
|
178
212
|
cpuModel: CpuModel.CMOS65C12,
|
|
@@ -184,6 +218,7 @@ export const allModels = [
|
|
|
184
218
|
}),
|
|
185
219
|
new Model({
|
|
186
220
|
name: "BBC Master 128 (ADFS)",
|
|
221
|
+
shortName: "Master 128",
|
|
187
222
|
synonyms: ["MasterADFS"],
|
|
188
223
|
os: ["master/mos3.20"],
|
|
189
224
|
cpuModel: CpuModel.CMOS65C12,
|
|
@@ -195,6 +230,7 @@ export const allModels = [
|
|
|
195
230
|
}),
|
|
196
231
|
new Model({
|
|
197
232
|
name: "BBC Master 128 (ANFS)",
|
|
233
|
+
shortName: "Master 128",
|
|
198
234
|
synonyms: ["MasterANFS"],
|
|
199
235
|
os: ["master/mos3.20"],
|
|
200
236
|
cpuModel: CpuModel.CMOS65C12,
|
package/src/music5000.js
CHANGED
package/src/ppia.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getKeyMapAtom } from "./
|
|
1
|
+
import { getKeyMapAtom } from "./keymap-atom.js";
|
|
2
2
|
|
|
3
3
|
// 8255 Programmable Peripheral Interface Adapter for the Acorn Atom.
|
|
4
4
|
// Reference: http://mdfs.net/Docs/Comp/Acorn/Atom/atap25.htm
|
|
@@ -415,9 +415,8 @@ export class AtomPPIA extends PPIA {
|
|
|
415
415
|
}
|
|
416
416
|
|
|
417
417
|
rewindTape() {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
418
|
+
this.stopTape();
|
|
419
|
+
if (this.tape) this.tape.rewind();
|
|
421
420
|
}
|
|
422
421
|
|
|
423
422
|
playTape() {
|