jsbeeb 1.11.0 → 1.12.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 +1 -1
- package/package.json +1 -2
- package/src/6502.js +7 -0
- package/src/app/app.js +1 -1
- package/src/app/electron.js +4 -4
- package/src/bem-snapshot.js +5 -184
- package/src/config.js +20 -9
- package/src/keyboard.js +17 -14
- package/src/main.js +47 -35
- package/src/models.js +4 -4
- package/src/snapshot-helpers.js +208 -0
- package/src/snapshot.js +9 -18
- package/src/uef-snapshot.js +402 -0
- package/src/utils.js +7 -1
- package/src/web/audio-handler.js +31 -14
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ sudo rpm -i out/dist/jsbeeb-1.0.1.x86_64.rpm
|
|
|
177
177
|
Doesn't support the sth: pseudo URL unlike `disc` and `tape`, but if given a ZIP file will attempt to use the `.rom`
|
|
178
178
|
file assumed to be within.
|
|
179
179
|
- (mostly internal use) `logFdcCommands`, `logFdcStateChanges` - turn on logging in the disc controller.
|
|
180
|
-
- `audioDebug
|
|
180
|
+
- `audioDebug` - show audio queue stats chart.
|
|
181
181
|
|
|
182
182
|
## Patches
|
|
183
183
|
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "jsbeeb",
|
|
8
8
|
"description": "Emulate a BBC Micro",
|
|
9
9
|
"repository": "git@github.com:mattgodbolt/jsbeeb.git",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.12.0",
|
|
11
11
|
"//": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=22.12.0"
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"argparse": "^2.0.1",
|
|
31
31
|
"bootstrap": "^5.3.8",
|
|
32
32
|
"bootswatch": "^5.3.8",
|
|
33
|
-
"event-emitter-es6": "^1.1.5",
|
|
34
33
|
"sharp": "^0.34.5",
|
|
35
34
|
"smoothie": "^1.36.1"
|
|
36
35
|
},
|
package/src/6502.js
CHANGED
|
@@ -1208,6 +1208,13 @@ export class Cpu6502 extends Base6502 {
|
|
|
1208
1208
|
if (state.roms) {
|
|
1209
1209
|
this.ramRomOs.set(state.roms.slice(0, 16 * 16384), this.romOffset);
|
|
1210
1210
|
}
|
|
1211
|
+
// Selectively overwrite individual sideways RAM banks (e.g. from BeebEm UEF import)
|
|
1212
|
+
// without touching ROM banks that jsbeeb has already loaded.
|
|
1213
|
+
if (state.swRamBanks) {
|
|
1214
|
+
for (const [bank, data] of Object.entries(state.swRamBanks)) {
|
|
1215
|
+
this.ramRomOs.set(data.slice(0, 16384), this.romOffset + Number(bank) * 16384);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1211
1218
|
this.videoDisplayPage = state.videoDisplayPage;
|
|
1212
1219
|
this.music5000PageSel = state.music5000PageSel;
|
|
1213
1220
|
|
package/src/app/app.js
CHANGED
|
@@ -242,7 +242,7 @@ const template = [
|
|
|
242
242
|
const result = await dialog.showOpenDialog(browserWindow, {
|
|
243
243
|
title: "Load emulator state",
|
|
244
244
|
filters: [
|
|
245
|
-
{ name: "Snapshot files", extensions: ["gz", "json", "snp"] },
|
|
245
|
+
{ name: "Snapshot files", extensions: ["gz", "json", "snp", "uef"] },
|
|
246
246
|
{ name: "All files", extensions: ["*"] },
|
|
247
247
|
],
|
|
248
248
|
properties: ["openFile"],
|
package/src/app/electron.js
CHANGED
|
@@ -55,11 +55,11 @@ function init(args) {
|
|
|
55
55
|
|
|
56
56
|
// Save settings when they change
|
|
57
57
|
if (config) {
|
|
58
|
-
config.
|
|
59
|
-
api.saveSettings(
|
|
58
|
+
config.addEventListener("settings-changed", (e) => {
|
|
59
|
+
api.saveSettings(e.detail);
|
|
60
60
|
});
|
|
61
|
-
config.
|
|
62
|
-
api.saveSettings(
|
|
61
|
+
config.addEventListener("media-changed", (e) => {
|
|
62
|
+
api.saveSettings(e.detail);
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
}
|
package/src/bem-snapshot.js
CHANGED
|
@@ -5,6 +5,8 @@ import { decompress } from "./utils.js";
|
|
|
5
5
|
// v1 (BEMSNAP1): Fixed-size 327,885 byte packed struct. Reference: beebjit state.c
|
|
6
6
|
// v3 (BEMSNAP3): Section-based with key+size headers, zlib-compressed memory. Reference: b-em savestate.c
|
|
7
7
|
|
|
8
|
+
import { volumeTable, buildVideoState, buildSnapshot } from "./snapshot-helpers.js";
|
|
9
|
+
|
|
8
10
|
const BemV1Size = 327885;
|
|
9
11
|
|
|
10
12
|
// v1 struct offsets
|
|
@@ -114,7 +116,7 @@ function convertViaState(bemVia, ic32) {
|
|
|
114
116
|
t2l: bemVia.t2l,
|
|
115
117
|
t1c: bemVia.t1c,
|
|
116
118
|
t2c: bemVia.t2c,
|
|
117
|
-
t1hit: !!bemVia.t1hit,
|
|
119
|
+
t1hit: bemVia.acr & 0x40 ? false : !!bemVia.t1hit,
|
|
118
120
|
t2hit: !!bemVia.t2hit,
|
|
119
121
|
portapins: 0xff,
|
|
120
122
|
portbpins: 0xff,
|
|
@@ -135,17 +137,6 @@ function convertViaState(bemVia, ic32) {
|
|
|
135
137
|
return result;
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
// Volume lookup table matching soundchip.js
|
|
139
|
-
const volumeTable = new Float32Array(16);
|
|
140
|
-
(() => {
|
|
141
|
-
let f = 1.0;
|
|
142
|
-
for (let i = 0; i < 15; ++i) {
|
|
143
|
-
volumeTable[i] = f / 4;
|
|
144
|
-
f *= Math.pow(10, -0.1);
|
|
145
|
-
}
|
|
146
|
-
volumeTable[15] = 0;
|
|
147
|
-
})();
|
|
148
|
-
|
|
149
140
|
function convertSoundState(snLatch, snCount, snStat, snVol, snNoise, snShift) {
|
|
150
141
|
const registers = new Uint16Array(4);
|
|
151
142
|
const counter = new Float32Array(4);
|
|
@@ -181,178 +172,6 @@ function convertSoundState(snLatch, snCount, snStat, snVol, snNoise, snShift) {
|
|
|
181
172
|
};
|
|
182
173
|
}
|
|
183
174
|
|
|
184
|
-
function buildVideoState(ulaControl, ulaPalette, crtcRegs, nulaCollook, crtcCounters) {
|
|
185
|
-
const regs = new Uint8Array(32);
|
|
186
|
-
regs.set(crtcRegs.slice(0, 18));
|
|
187
|
-
const actualPal = new Uint8Array(16);
|
|
188
|
-
for (let i = 0; i < 16; i++) actualPal[i] = ulaPalette[i] & 0x0f;
|
|
189
|
-
|
|
190
|
-
// Use NULA collook if provided, otherwise use default BBC palette
|
|
191
|
-
const collook =
|
|
192
|
-
nulaCollook ||
|
|
193
|
-
new Int32Array([
|
|
194
|
-
0xff000000, 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff, 0xff000000,
|
|
195
|
-
0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff,
|
|
196
|
-
]);
|
|
197
|
-
|
|
198
|
-
// Compute ulaPal from actualPal + collook, matching jsbeeb's Ula._recomputeUlaPal
|
|
199
|
-
const flashEnabled = !!(ulaControl & 1);
|
|
200
|
-
const defaultFlash = new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]);
|
|
201
|
-
const flash = defaultFlash;
|
|
202
|
-
const ulaPal = new Int32Array(16);
|
|
203
|
-
for (let i = 0; i < 16; i++) {
|
|
204
|
-
const palVal = actualPal[i];
|
|
205
|
-
let colour = collook[(palVal & 0xf) ^ 7];
|
|
206
|
-
if (palVal & 8 && flashEnabled && flash[(palVal & 7) ^ 7]) {
|
|
207
|
-
colour = collook[palVal & 0xf];
|
|
208
|
-
}
|
|
209
|
-
ulaPal[i] = colour;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
regs,
|
|
214
|
-
bitmapX: 0,
|
|
215
|
-
bitmapY: 0,
|
|
216
|
-
oddClock: false,
|
|
217
|
-
frameCount: 0,
|
|
218
|
-
doEvenFrameLogic: false,
|
|
219
|
-
isEvenRender: true,
|
|
220
|
-
lastRenderWasEven: false,
|
|
221
|
-
firstScanline: true,
|
|
222
|
-
inHSync: false,
|
|
223
|
-
inVSync: false,
|
|
224
|
-
hadVSyncThisRow: false,
|
|
225
|
-
checkVertAdjust: false,
|
|
226
|
-
endOfMainLatched: false,
|
|
227
|
-
endOfVertAdjustLatched: false,
|
|
228
|
-
endOfFrameLatched: false,
|
|
229
|
-
inVertAdjust: false,
|
|
230
|
-
inDummyRaster: false,
|
|
231
|
-
hpulseWidth: regs[3] & 0x0f,
|
|
232
|
-
vpulseWidth: (regs[3] & 0xf0) >>> 4,
|
|
233
|
-
hpulseCounter: 0,
|
|
234
|
-
vpulseCounter: 0,
|
|
235
|
-
dispEnabled: 0x3f,
|
|
236
|
-
horizCounter: crtcCounters ? crtcCounters.hc : 0,
|
|
237
|
-
vertCounter: crtcCounters ? crtcCounters.vc : 0,
|
|
238
|
-
scanlineCounter: crtcCounters ? crtcCounters.sc : 0,
|
|
239
|
-
vertAdjustCounter: 0,
|
|
240
|
-
addr: crtcCounters ? crtcCounters.ma : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
241
|
-
lineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
242
|
-
nextLineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
|
|
243
|
-
ulactrl: ulaControl,
|
|
244
|
-
pixelsPerChar: ulaControl & 0x10 ? 8 : 16,
|
|
245
|
-
halfClock: !(ulaControl & 0x10),
|
|
246
|
-
ulaMode: (ulaControl >>> 2) & 3,
|
|
247
|
-
teletextMode: !!(ulaControl & 2),
|
|
248
|
-
displayEnableSkew: Math.min((regs[8] & 0x30) >>> 4, 2),
|
|
249
|
-
ulaPal,
|
|
250
|
-
actualPal,
|
|
251
|
-
cursorOn: false,
|
|
252
|
-
cursorOff: false,
|
|
253
|
-
cursorOnThisFrame: false,
|
|
254
|
-
cursorDrawIndex: 0,
|
|
255
|
-
cursorPos: (regs[15] | (regs[14] << 8)) & 0x3fff,
|
|
256
|
-
interlacedSyncAndVideo: (regs[8] & 3) === 3,
|
|
257
|
-
screenSubtract: 0,
|
|
258
|
-
ula: {
|
|
259
|
-
collook: collook.slice(),
|
|
260
|
-
flash: new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]),
|
|
261
|
-
paletteWriteFlag: false,
|
|
262
|
-
paletteFirstByte: 0,
|
|
263
|
-
paletteMode: 0,
|
|
264
|
-
horizontalOffset: 0,
|
|
265
|
-
leftBlank: 0,
|
|
266
|
-
disabled: false,
|
|
267
|
-
attributeMode: 0,
|
|
268
|
-
attributeText: 0,
|
|
269
|
-
},
|
|
270
|
-
crtc: { curReg: 0 },
|
|
271
|
-
teletext: {
|
|
272
|
-
prevCol: 0,
|
|
273
|
-
col: 7,
|
|
274
|
-
bg: 0,
|
|
275
|
-
sep: false,
|
|
276
|
-
dbl: false,
|
|
277
|
-
oldDbl: false,
|
|
278
|
-
secondHalfOfDouble: false,
|
|
279
|
-
wasDbl: false,
|
|
280
|
-
gfx: false,
|
|
281
|
-
flash: false,
|
|
282
|
-
flashOn: false,
|
|
283
|
-
flashTime: 0,
|
|
284
|
-
heldChar: 0,
|
|
285
|
-
holdChar: false,
|
|
286
|
-
dataQueue: [0, 0, 0, 0],
|
|
287
|
-
scanlineCounter: 0,
|
|
288
|
-
levelDEW: false,
|
|
289
|
-
levelDISPTMG: false,
|
|
290
|
-
levelRA0: false,
|
|
291
|
-
nextGlyphs: "normal",
|
|
292
|
-
curGlyphs: "normal",
|
|
293
|
-
heldGlyphs: "normal",
|
|
294
|
-
},
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const DefaultAcia = {
|
|
299
|
-
sr: 0x02,
|
|
300
|
-
cr: 0x00,
|
|
301
|
-
dr: 0x00,
|
|
302
|
-
rs423Selected: false,
|
|
303
|
-
motorOn: false,
|
|
304
|
-
tapeCarrierCount: 0,
|
|
305
|
-
tapeDcdLineLevel: false,
|
|
306
|
-
hadDcdHigh: false,
|
|
307
|
-
serialReceiveRate: 19200,
|
|
308
|
-
serialReceiveCyclesPerByte: 0,
|
|
309
|
-
txCompleteTaskOffset: null,
|
|
310
|
-
runTapeTaskOffset: null,
|
|
311
|
-
runRs423TaskOffset: null,
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
const DefaultAdc = { status: 0x40, low: 0x00, high: 0x00, taskOffset: null };
|
|
315
|
-
|
|
316
|
-
function buildSnapshot(modelName, cpuState, ram, roms, sysvia, uservia, video, soundChip) {
|
|
317
|
-
return {
|
|
318
|
-
format: "jsbeeb-snapshot",
|
|
319
|
-
version: 1,
|
|
320
|
-
model: modelName,
|
|
321
|
-
timestamp: new Date().toISOString(),
|
|
322
|
-
importedFrom: "b-em",
|
|
323
|
-
state: {
|
|
324
|
-
a: cpuState.a,
|
|
325
|
-
x: cpuState.x,
|
|
326
|
-
y: cpuState.y,
|
|
327
|
-
s: cpuState.s,
|
|
328
|
-
pc: cpuState.pc,
|
|
329
|
-
p: cpuState.flags | 0x30,
|
|
330
|
-
nmiLevel: !!cpuState.nmi,
|
|
331
|
-
nmiEdge: false,
|
|
332
|
-
halted: false,
|
|
333
|
-
takeInt: false,
|
|
334
|
-
romsel: cpuState.fe30 ?? 0,
|
|
335
|
-
acccon: cpuState.fe34 ?? 0,
|
|
336
|
-
videoDisplayPage: 0,
|
|
337
|
-
currentCycles: 0,
|
|
338
|
-
targetCycles: 0,
|
|
339
|
-
cycleSeconds: 0,
|
|
340
|
-
peripheralCycles: 0,
|
|
341
|
-
videoCycles: 0,
|
|
342
|
-
music5000PageSel: 0,
|
|
343
|
-
ram,
|
|
344
|
-
roms,
|
|
345
|
-
scheduler: { epoch: 0 },
|
|
346
|
-
sysvia,
|
|
347
|
-
uservia,
|
|
348
|
-
video,
|
|
349
|
-
soundChip,
|
|
350
|
-
acia: { ...DefaultAcia },
|
|
351
|
-
adc: { ...DefaultAdc },
|
|
352
|
-
},
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
175
|
// ── V1 parser (BEMSNAP1, fixed struct) ──────────────────────────────
|
|
357
176
|
|
|
358
177
|
function parseBemV1(buffer) {
|
|
@@ -402,6 +221,7 @@ function parseBemV1(buffer) {
|
|
|
402
221
|
const snShift = view.getUint16(soundOff + 53, true);
|
|
403
222
|
|
|
404
223
|
return buildSnapshot(
|
|
224
|
+
"b-em",
|
|
405
225
|
"B",
|
|
406
226
|
cpuState,
|
|
407
227
|
ram,
|
|
@@ -637,6 +457,7 @@ function finishV3Parse(modelName, cpuState, ram, roms, sections) {
|
|
|
637
457
|
}
|
|
638
458
|
|
|
639
459
|
return buildSnapshot(
|
|
460
|
+
"b-em",
|
|
640
461
|
modelName,
|
|
641
462
|
cpuState,
|
|
642
463
|
ram,
|
package/src/config.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
import
|
|
3
|
-
import { findModel } from "./models.js";
|
|
2
|
+
import { allModels, findModel } from "./models.js";
|
|
4
3
|
import { getFilterForMode } from "./canvas.js";
|
|
5
4
|
|
|
6
|
-
export class Config extends
|
|
5
|
+
export class Config extends EventTarget {
|
|
7
6
|
constructor(onChange, onClose) {
|
|
8
7
|
super();
|
|
9
8
|
this.onChange = onChange;
|
|
@@ -25,16 +24,28 @@ export class Config extends EventEmitter {
|
|
|
25
24
|
configuration.addEventListener("hide.bs.modal", () => {
|
|
26
25
|
this.onClose(this.changed);
|
|
27
26
|
if (Object.keys(this.changed).length > 0) {
|
|
28
|
-
this.
|
|
27
|
+
this.dispatchEvent(new CustomEvent("settings-changed", { detail: this.changed }));
|
|
29
28
|
}
|
|
30
29
|
});
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
const modelMenu = document.querySelector(".model-menu");
|
|
32
|
+
for (const model of allModels) {
|
|
33
|
+
if (model.synonyms.length === 0) continue; // skip non-selectable models (e.g. Tube65C02)
|
|
34
|
+
const li = document.createElement("li");
|
|
35
|
+
const a = document.createElement("a");
|
|
36
|
+
a.href = "#";
|
|
37
|
+
a.className = "dropdown-item";
|
|
38
|
+
a.dataset.target = model.synonyms[0];
|
|
39
|
+
a.textContent = model.name;
|
|
40
|
+
li.appendChild(a);
|
|
41
|
+
modelMenu.appendChild(li);
|
|
37
42
|
}
|
|
43
|
+
modelMenu.addEventListener("click", (e) => {
|
|
44
|
+
const link = e.target.closest("a[data-target]");
|
|
45
|
+
if (!link) return;
|
|
46
|
+
this.changed.model = link.dataset.target;
|
|
47
|
+
this.setDropdownText(link.textContent);
|
|
48
|
+
});
|
|
38
49
|
|
|
39
50
|
document.getElementById("65c02").addEventListener("click", () => {
|
|
40
51
|
this.changed.coProcessor = document.getElementById("65c02").checked;
|
package/src/keyboard.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
import * as utils from "./utils.js";
|
|
3
|
-
import EventEmitter from "event-emitter-es6";
|
|
4
3
|
|
|
5
4
|
const isMac = typeof window !== "undefined" && /^Mac/i.test(window.navigator?.platform || "");
|
|
6
5
|
|
|
@@ -15,7 +14,7 @@ const isMac = typeof window !== "undefined" && /^Mac/i.test(window.navigator?.pl
|
|
|
15
14
|
/**
|
|
16
15
|
* Keyboard class that handles all keyboard related functionality
|
|
17
16
|
*/
|
|
18
|
-
export class Keyboard extends
|
|
17
|
+
export class Keyboard extends EventTarget {
|
|
19
18
|
/**
|
|
20
19
|
* Create a new Keyboard instance with specified configuration
|
|
21
20
|
* @param {KeyboardConfig} config - The configuration object
|
|
@@ -182,7 +181,7 @@ export class Keyboard extends EventEmitter {
|
|
|
182
181
|
// Handle debugger 'g' key press
|
|
183
182
|
if (this.dbgr.enabled() && code === LOWERCASE_G) {
|
|
184
183
|
this.dbgr.hide();
|
|
185
|
-
this.
|
|
184
|
+
this.dispatchEvent(new Event("resume"));
|
|
186
185
|
return;
|
|
187
186
|
}
|
|
188
187
|
|
|
@@ -193,7 +192,7 @@ export class Keyboard extends EventEmitter {
|
|
|
193
192
|
return;
|
|
194
193
|
} else if (code === LOWERCASE_N) {
|
|
195
194
|
this.requestStep();
|
|
196
|
-
this.
|
|
195
|
+
this.dispatchEvent(new Event("resume"));
|
|
197
196
|
return;
|
|
198
197
|
}
|
|
199
198
|
}
|
|
@@ -247,7 +246,7 @@ export class Keyboard extends EventEmitter {
|
|
|
247
246
|
*/
|
|
248
247
|
_handleSpecialKeys(code) {
|
|
249
248
|
if (code === utils.keyCodes.F12 || code === utils.keyCodes.BREAK) {
|
|
250
|
-
this.
|
|
249
|
+
this.dispatchEvent(new CustomEvent("break", { detail: true }));
|
|
251
250
|
this.processor.setReset(true);
|
|
252
251
|
return true;
|
|
253
252
|
} else if (isMac && code === utils.keyCodes.CAPSLOCK) {
|
|
@@ -278,7 +277,7 @@ export class Keyboard extends EventEmitter {
|
|
|
278
277
|
|
|
279
278
|
// Handle special key cases
|
|
280
279
|
if (code === utils.keyCodes.F12 || code === utils.keyCodes.BREAK) {
|
|
281
|
-
this.
|
|
280
|
+
this.dispatchEvent(new CustomEvent("break", { detail: false }));
|
|
282
281
|
this.processor.setReset(false);
|
|
283
282
|
return;
|
|
284
283
|
} else if (isMac && code === utils.keyCodes.CAPSLOCK) {
|
|
@@ -308,14 +307,18 @@ export class Keyboard extends EventEmitter {
|
|
|
308
307
|
setTimeout(() => this.processor.sysvia.keyUp(utils.keyCodes.CAPSLOCK), CAPS_LOCK_DELAY);
|
|
309
308
|
|
|
310
309
|
if (isMac && window.localStorage && !window.localStorage.getItem("warnedAboutRubbishMacs")) {
|
|
311
|
-
this.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
this.dispatchEvent(
|
|
311
|
+
new CustomEvent("showError", {
|
|
312
|
+
detail: {
|
|
313
|
+
context: "handling caps lock on Mac OS X",
|
|
314
|
+
error: `Mac OS X does not generate key up events for caps lock presses.
|
|
315
|
+
jsbeeb can only simulate a 'tap' of the caps lock key. This means it doesn't work well for games
|
|
316
|
+
that use caps lock for left or fire, as we can't tell if it's being held down. If you need to play
|
|
316
317
|
such a game, please see the documentation about remapping keys.
|
|
317
318
|
Close this window to continue (you won't see this error again)`,
|
|
318
|
-
|
|
319
|
+
},
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
319
322
|
window.localStorage.setItem("warnedAboutRubbishMacs", "true");
|
|
320
323
|
}
|
|
321
324
|
}
|
|
@@ -437,7 +440,7 @@ export class Keyboard extends EventEmitter {
|
|
|
437
440
|
*/
|
|
438
441
|
pauseEmulation() {
|
|
439
442
|
this.pauseEmu = true;
|
|
440
|
-
this.
|
|
443
|
+
this.dispatchEvent(new Event("pause"));
|
|
441
444
|
}
|
|
442
445
|
|
|
443
446
|
/**
|
|
@@ -445,6 +448,6 @@ export class Keyboard extends EventEmitter {
|
|
|
445
448
|
*/
|
|
446
449
|
resumeEmulation() {
|
|
447
450
|
this.pauseEmu = false;
|
|
448
|
-
this.
|
|
451
|
+
this.dispatchEvent(new Event("resume"));
|
|
449
452
|
}
|
|
450
453
|
}
|
package/src/main.js
CHANGED
|
@@ -28,8 +28,9 @@ import { MicrophoneInput } from "./microphone-input.js";
|
|
|
28
28
|
import { SpeechOutput } from "./speech-output.js";
|
|
29
29
|
import { MouseJoystickSource } from "./mouse-joystick-source.js";
|
|
30
30
|
import { getFilterForMode } from "./canvas.js";
|
|
31
|
-
import { createSnapshot, restoreSnapshot, snapshotToJSON, snapshotFromJSON,
|
|
31
|
+
import { createSnapshot, restoreSnapshot, snapshotToJSON, snapshotFromJSON, isSameModel } from "./snapshot.js";
|
|
32
32
|
import { isBemSnapshot, parseBemSnapshot } from "./bem-snapshot.js";
|
|
33
|
+
import { isUefSnapshot, parseUefSnapshot } from "./uef-snapshot.js";
|
|
33
34
|
import { RewindBuffer } from "./rewind.js";
|
|
34
35
|
import { RewindUI } from "./rewind-ui.js";
|
|
35
36
|
import {
|
|
@@ -102,6 +103,7 @@ const paramTypes = {
|
|
|
102
103
|
coProcessor: ParamTypes.BOOL,
|
|
103
104
|
mouseJoystickEnabled: ParamTypes.BOOL,
|
|
104
105
|
speechOutput: ParamTypes.BOOL,
|
|
106
|
+
audioDebug: ParamTypes.BOOL,
|
|
105
107
|
|
|
106
108
|
// Numeric parameters
|
|
107
109
|
speed: ParamTypes.INT,
|
|
@@ -259,7 +261,7 @@ const config = new Config(
|
|
|
259
261
|
updateAdcSources(parsedQuery.mouseJoystickEnabled, parsedQuery.microphoneChannel);
|
|
260
262
|
|
|
261
263
|
if (changed.microphoneChannel !== undefined) {
|
|
262
|
-
setupMicrophone()
|
|
264
|
+
setupMicrophone();
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
if (changed.speechOutput !== undefined) {
|
|
@@ -383,7 +385,9 @@ video = new Video(model.isMaster, canvas.fb32, function paint(minx, miny, maxx,
|
|
|
383
385
|
});
|
|
384
386
|
if (parsedQuery.fakeVideo !== undefined) video = new FakeVideo();
|
|
385
387
|
|
|
386
|
-
const
|
|
388
|
+
const audioStatsEl = document.getElementById("audio-stats");
|
|
389
|
+
if (audioStatsEl) audioStatsEl.hidden = !parsedQuery.audioDebug;
|
|
390
|
+
const audioStatsNode = parsedQuery.audioDebug ? audioStatsEl : null;
|
|
387
391
|
const audioHandler = new AudioHandler({
|
|
388
392
|
warningNode: document.getElementById("audio-warning"),
|
|
389
393
|
statsNode: audioStatsNode,
|
|
@@ -391,7 +395,6 @@ const audioHandler = new AudioHandler({
|
|
|
391
395
|
audioFilterQ,
|
|
392
396
|
noSeek,
|
|
393
397
|
});
|
|
394
|
-
if (!parsedQuery.audioDebug) audioStatsNode.style.display = "none";
|
|
395
398
|
// Firefox will report that audio is suspended even when it will
|
|
396
399
|
// start playing without user interaction, so we need to delay a
|
|
397
400
|
// little to get a reliable indication.
|
|
@@ -492,8 +495,12 @@ pastetext.addEventListener("dragover", function (event) {
|
|
|
492
495
|
pastetext.addEventListener("drop", async function (event) {
|
|
493
496
|
utils.noteEvent("local", "drop");
|
|
494
497
|
const file = event.dataTransfer.files[0];
|
|
495
|
-
|
|
496
|
-
|
|
498
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
499
|
+
if (isSnapshotFile(file.name, arrayBuffer)) {
|
|
500
|
+
await loadStateFromFile(file, arrayBuffer);
|
|
501
|
+
} else if (file.name.toLowerCase().endsWith(".uef")) {
|
|
502
|
+
// Regular UEF tape image (not a BeebEm save state)
|
|
503
|
+
processor.acia.setTape(loadTapeFromData(file.name, new Uint8Array(arrayBuffer)));
|
|
497
504
|
} else {
|
|
498
505
|
await loadHTMLFile(file);
|
|
499
506
|
}
|
|
@@ -501,7 +508,7 @@ pastetext.addEventListener("drop", async function (event) {
|
|
|
501
508
|
|
|
502
509
|
const cubMonitor = document.getElementById("cub-monitor");
|
|
503
510
|
function onCubMouseEvent(evt) {
|
|
504
|
-
audioHandler.tryResume()
|
|
511
|
+
audioHandler.tryResume();
|
|
505
512
|
if (document.activeElement !== document.body) document.activeElement.blur();
|
|
506
513
|
const cubRect = cubMonitor.getBoundingClientRect();
|
|
507
514
|
const screenRect = screenCanvas.getBoundingClientRect();
|
|
@@ -737,12 +744,12 @@ keyboard = new Keyboard({
|
|
|
737
744
|
keyLayout,
|
|
738
745
|
dbgr,
|
|
739
746
|
});
|
|
740
|
-
keyboard.
|
|
741
|
-
keyboard.
|
|
742
|
-
keyboard.
|
|
743
|
-
keyboard.
|
|
747
|
+
keyboard.addEventListener("showError", (e) => showError(e.detail.context, e.detail.error));
|
|
748
|
+
keyboard.addEventListener("pause", () => stop(false));
|
|
749
|
+
keyboard.addEventListener("resume", () => go());
|
|
750
|
+
keyboard.addEventListener("break", (e) => {
|
|
744
751
|
// F12/Break: Reset processor
|
|
745
|
-
if (
|
|
752
|
+
if (e.detail) utils.noteEvent("keyboard", "press", "break");
|
|
746
753
|
});
|
|
747
754
|
|
|
748
755
|
// Register default key handlers
|
|
@@ -846,8 +853,8 @@ keyboard.registerKeyHandler(
|
|
|
846
853
|
|
|
847
854
|
// Setup key handlers
|
|
848
855
|
document.addEventListener("keydown", (evt) => {
|
|
849
|
-
audioHandler.tryResume()
|
|
850
|
-
ensureMicrophoneRunning()
|
|
856
|
+
audioHandler.tryResume();
|
|
857
|
+
ensureMicrophoneRunning();
|
|
851
858
|
keyboard.keyDown(evt);
|
|
852
859
|
});
|
|
853
860
|
document.addEventListener("keypress", (evt) => keyboard.keyPress(evt));
|
|
@@ -857,19 +864,19 @@ function setDisc1Image(name) {
|
|
|
857
864
|
delete parsedQuery.disc;
|
|
858
865
|
parsedQuery.disc1 = name;
|
|
859
866
|
updateUrl();
|
|
860
|
-
config.
|
|
867
|
+
config.dispatchEvent(new CustomEvent("media-changed", { detail: { disc1: name } }));
|
|
861
868
|
}
|
|
862
869
|
|
|
863
870
|
function setDisc2Image(name) {
|
|
864
871
|
parsedQuery.disc2 = name;
|
|
865
872
|
updateUrl();
|
|
866
|
-
config.
|
|
873
|
+
config.dispatchEvent(new CustomEvent("media-changed", { detail: { disc2: name } }));
|
|
867
874
|
}
|
|
868
875
|
|
|
869
876
|
function setTapeImage(name) {
|
|
870
877
|
parsedQuery.tape = name;
|
|
871
878
|
updateUrl();
|
|
872
|
-
config.
|
|
879
|
+
config.dispatchEvent(new CustomEvent("media-changed", { detail: { tape: name } }));
|
|
873
880
|
}
|
|
874
881
|
|
|
875
882
|
function sthClearList() {
|
|
@@ -1354,13 +1361,12 @@ googleDriveEl.addEventListener("show.bs.modal", async function () {
|
|
|
1354
1361
|
row.classList.remove("template");
|
|
1355
1362
|
dbList.appendChild(row);
|
|
1356
1363
|
row.querySelector(".name").textContent = item.name;
|
|
1357
|
-
row.addEventListener("click", function () {
|
|
1364
|
+
row.addEventListener("click", async function () {
|
|
1358
1365
|
utils.noteEvent("google-drive", "click", item.name);
|
|
1359
1366
|
setDisc1Image(`gd:${item.id}/${item.name}`);
|
|
1360
|
-
gdLoad(item).then(function (ssd) {
|
|
1361
|
-
processor.fdc.loadDisc(0, ssd);
|
|
1362
|
-
});
|
|
1363
1367
|
googleDriveModal.hide();
|
|
1368
|
+
const ssd = await gdLoad(item);
|
|
1369
|
+
if (ssd) processor.fdc.loadDisc(0, ssd);
|
|
1364
1370
|
});
|
|
1365
1371
|
}
|
|
1366
1372
|
});
|
|
@@ -1372,13 +1378,11 @@ for (const image of availableImages) {
|
|
|
1372
1378
|
discList.appendChild(elem);
|
|
1373
1379
|
elem.querySelector(".name").textContent = image.name;
|
|
1374
1380
|
elem.querySelector(".description").textContent = image.desc;
|
|
1375
|
-
elem.addEventListener("click", function () {
|
|
1381
|
+
elem.addEventListener("click", async function () {
|
|
1376
1382
|
utils.noteEvent("images", "click", image.file);
|
|
1377
1383
|
setDisc1Image(image.file);
|
|
1378
|
-
loadDiscImage(parsedQuery.disc1).then(function (disc) {
|
|
1379
|
-
processor.fdc.loadDisc(0, disc);
|
|
1380
|
-
});
|
|
1381
1384
|
$discsModal.hide();
|
|
1385
|
+
processor.fdc.loadDisc(0, await loadDiscImage(parsedQuery.disc1));
|
|
1382
1386
|
});
|
|
1383
1387
|
}
|
|
1384
1388
|
|
|
@@ -1501,14 +1505,16 @@ document.getElementById("save-state").addEventListener("click", async function (
|
|
|
1501
1505
|
if (wasRunning) go();
|
|
1502
1506
|
});
|
|
1503
1507
|
|
|
1504
|
-
async function loadStateFromFile(file) {
|
|
1508
|
+
async function loadStateFromFile(file, preReadBuffer) {
|
|
1505
1509
|
const wasRunning = running;
|
|
1506
1510
|
if (running) stop(false);
|
|
1507
1511
|
try {
|
|
1508
|
-
const arrayBuffer = await file.arrayBuffer();
|
|
1512
|
+
const arrayBuffer = preReadBuffer || (await file.arrayBuffer());
|
|
1509
1513
|
let snapshot;
|
|
1510
1514
|
if (isBemSnapshot(arrayBuffer)) {
|
|
1511
1515
|
snapshot = await parseBemSnapshot(arrayBuffer);
|
|
1516
|
+
} else if (isUefSnapshot(arrayBuffer)) {
|
|
1517
|
+
snapshot = parseUefSnapshot(arrayBuffer);
|
|
1512
1518
|
} else {
|
|
1513
1519
|
// Detect gzip (magic bytes 0x1f 0x8b) or plain JSON
|
|
1514
1520
|
const bytes = new Uint8Array(arrayBuffer);
|
|
@@ -1521,7 +1527,7 @@ async function loadStateFromFile(file) {
|
|
|
1521
1527
|
}
|
|
1522
1528
|
snapshot = snapshotFromJSON(text);
|
|
1523
1529
|
}
|
|
1524
|
-
if (!
|
|
1530
|
+
if (!isSameModel(snapshot.model, model.name)) {
|
|
1525
1531
|
// Model mismatch: stash state and reload with correct model
|
|
1526
1532
|
sessionStorage.setItem("jsbeeb-pending-state", snapshotToJSON(snapshot));
|
|
1527
1533
|
const newQuery = { ...parsedQuery, model: snapshot.model };
|
|
@@ -1541,9 +1547,13 @@ async function loadStateFromFile(file) {
|
|
|
1541
1547
|
if (wasRunning) go();
|
|
1542
1548
|
}
|
|
1543
1549
|
|
|
1544
|
-
function isSnapshotFile(filename) {
|
|
1550
|
+
function isSnapshotFile(filename, arrayBuffer) {
|
|
1545
1551
|
const lower = filename.toLowerCase();
|
|
1546
|
-
|
|
1552
|
+
if (lower.endsWith(".snp") || lower.endsWith(".json") || lower.endsWith(".json.gz") || lower.endsWith(".gz"))
|
|
1553
|
+
return true;
|
|
1554
|
+
// .uef can be either a BeebEm save state or a regular tape image - check content
|
|
1555
|
+
if (lower.endsWith(".uef") && arrayBuffer) return isUefSnapshot(arrayBuffer);
|
|
1556
|
+
return false;
|
|
1547
1557
|
}
|
|
1548
1558
|
|
|
1549
1559
|
document.getElementById("load-state").addEventListener("change", async function (event) {
|
|
@@ -1684,8 +1694,10 @@ const startPromise = (async () => {
|
|
|
1684
1694
|
return Promise.all(imageLoads);
|
|
1685
1695
|
})();
|
|
1686
1696
|
|
|
1687
|
-
|
|
1688
|
-
|
|
1697
|
+
(async () => {
|
|
1698
|
+
try {
|
|
1699
|
+
await startPromise;
|
|
1700
|
+
|
|
1689
1701
|
switch (needsAutoboot) {
|
|
1690
1702
|
case "boot":
|
|
1691
1703
|
sthAutoboot.checked = true;
|
|
@@ -1726,11 +1738,11 @@ startPromise
|
|
|
1726
1738
|
}
|
|
1727
1739
|
|
|
1728
1740
|
go();
|
|
1729
|
-
})
|
|
1730
|
-
.catch((error) => {
|
|
1741
|
+
} catch (error) {
|
|
1731
1742
|
console.error("Error initialising emulator:", error);
|
|
1732
1743
|
showError("initialising", error);
|
|
1733
|
-
}
|
|
1744
|
+
}
|
|
1745
|
+
})();
|
|
1734
1746
|
|
|
1735
1747
|
const aysEl = document.getElementById("are-you-sure");
|
|
1736
1748
|
const aysModal = new bootstrap.Modal(aysEl);
|