jsbeeb 1.23.0 → 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 +14 -10
- package/src/6502.js +39 -42
- package/src/6502.opcodes.js +7 -13
- 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/{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 +66 -180
- package/src/media-resolver.js +68 -0
- package/src/mmc.js +3 -2
- package/src/models.js +17 -3
- 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 +41 -74
- package/src/web/console-surface.js +25 -0
- package/src/web/debug.js +6 -5
- 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/keyboard-setup.js +18 -38
- 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
|
@@ -1,181 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// Minimal ZIP extractor. Supports methods 0 (stored) and 8 (deflate); other
|
|
3
|
-
// methods (bzip2, lzma, etc.) will throw an error with the method number.
|
|
4
|
-
|
|
5
|
-
import { inflate as pakoInflate, inflateRaw as pakoInflateRaw, ungzip as pakoUngzip } from "pako";
|
|
6
|
-
|
|
7
|
-
const ZipLocalHeaderSig = 0x04034b50;
|
|
8
|
-
const ZipCentralDirSig = 0x02014b50;
|
|
9
|
-
const ZipEocdSig = 0x06054b50;
|
|
10
|
-
const ZipMethodStored = 0;
|
|
11
|
-
const ZipMethodDeflate = 8;
|
|
12
|
-
|
|
13
|
-
function readU16(buf, off) {
|
|
14
|
-
return buf[off] | (buf[off + 1] << 8);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function readU32(buf, off) {
|
|
18
|
-
return (buf[off] | (buf[off + 1] << 8) | (buf[off + 2] << 16) | (buf[off + 3] << 24)) >>> 0;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isValidEocd(buf, off) {
|
|
22
|
-
const commentLen = readU16(buf, off + 20);
|
|
23
|
-
if (off + 22 + commentLen !== buf.length) return false;
|
|
24
|
-
const cdOff = readU32(buf, off + 16);
|
|
25
|
-
const cdSize = readU32(buf, off + 12);
|
|
26
|
-
return cdOff <= off && cdSize <= off - cdOff;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Find the End of Central Directory record by scanning backwards.
|
|
30
|
-
// EOCD is in the last 65557 bytes (22-byte fixed record + up to 65535-byte comment).
|
|
31
|
-
function findEocd(buf) {
|
|
32
|
-
const searchStart = Math.max(0, buf.length - 65557);
|
|
33
|
-
for (let i = buf.length - 22; i >= searchStart; i--) {
|
|
34
|
-
if (readU32(buf, i) === ZipEocdSig && isValidEocd(buf, i)) return i;
|
|
35
|
-
}
|
|
36
|
-
throw new Error("Not a ZIP file: EOCD not found");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function inflateWith(inflater, data, context) {
|
|
40
|
-
if (!(data instanceof Uint8Array)) data = new Uint8Array(data);
|
|
41
|
-
try {
|
|
42
|
-
return inflater(data);
|
|
43
|
-
} catch (cause) {
|
|
44
|
-
throw new Error(`${context}: ${cause.message || cause}`, { cause });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function inflate(data) {
|
|
49
|
-
return inflateWith(pakoInflate, data, "Unable to inflate");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Extract all files from a ZIP archive. Returns {filename: Uint8Array}.
|
|
53
|
-
export async function unzip(buf) {
|
|
54
|
-
if (!(buf instanceof Uint8Array)) buf = new Uint8Array(buf);
|
|
55
|
-
const eocdOff = findEocd(buf);
|
|
56
|
-
const cdOff = readU32(buf, eocdOff + 16);
|
|
57
|
-
const cdCount = readU16(buf, eocdOff + 10);
|
|
58
|
-
const decoder = new TextDecoder();
|
|
59
|
-
|
|
60
|
-
const files = Object.create(null);
|
|
61
|
-
let pos = cdOff;
|
|
62
|
-
for (let i = 0; i < cdCount; i++) {
|
|
63
|
-
if (pos + 46 > buf.length || readU32(buf, pos) !== ZipCentralDirSig)
|
|
64
|
-
throw new Error("Bad central directory entry");
|
|
65
|
-
const flags = readU16(buf, pos + 8);
|
|
66
|
-
if (flags & 0x0001) throw new Error("Encrypted ZIP entries are not supported");
|
|
67
|
-
const method = readU16(buf, pos + 10);
|
|
68
|
-
const expectedCrc = readU32(buf, pos + 16);
|
|
69
|
-
const compressedSize = readU32(buf, pos + 20);
|
|
70
|
-
const nameLen = readU16(buf, pos + 28);
|
|
71
|
-
const extraLen = readU16(buf, pos + 30);
|
|
72
|
-
const commentLen = readU16(buf, pos + 32);
|
|
73
|
-
const localHeaderOff = readU32(buf, pos + 42);
|
|
74
|
-
const name = decoder.decode(buf.subarray(pos + 46, pos + 46 + nameLen));
|
|
75
|
-
pos += 46 + nameLen + extraLen + commentLen;
|
|
76
|
-
|
|
77
|
-
// Read local file header to find actual data offset.
|
|
78
|
-
if (readU32(buf, localHeaderOff) !== ZipLocalHeaderSig) throw new Error(`Bad local file header for ${name}`);
|
|
79
|
-
const localNameLen = readU16(buf, localHeaderOff + 26);
|
|
80
|
-
const localExtraLen = readU16(buf, localHeaderOff + 28);
|
|
81
|
-
const dataOff = localHeaderOff + 30 + localNameLen + localExtraLen;
|
|
82
|
-
const dataEnd = dataOff + compressedSize;
|
|
83
|
-
if (dataEnd > buf.length) throw new Error(`Truncated ZIP entry data for ${name}`);
|
|
84
|
-
const raw = buf.subarray(dataOff, dataEnd);
|
|
85
|
-
|
|
86
|
-
if (method === ZipMethodStored) {
|
|
87
|
-
files[name] = raw.slice();
|
|
88
|
-
} else if (method === ZipMethodDeflate) {
|
|
89
|
-
files[name] = inflateWith(pakoInflateRaw, raw, `Unable to inflate ZIP entry ${name}`);
|
|
90
|
-
} else {
|
|
91
|
-
throw new Error(`Unsupported ZIP compression method ${method} for ${name}`);
|
|
92
|
-
}
|
|
93
|
-
if (crc32(files[name]) >>> 0 !== expectedCrc) throw new Error(`Corrupt ZIP entry ${name}: CRC32 mismatch`);
|
|
94
|
-
}
|
|
95
|
-
return files;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Standard CRC-32/ISO-HDLC.
|
|
99
|
-
const Crc32Table = new Uint32Array(256).map((_, index) => {
|
|
100
|
-
let crc = index;
|
|
101
|
-
for (let bit = 0; bit < 8; ++bit) crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
102
|
-
return crc;
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
export function crc32(data) {
|
|
106
|
-
let crc = 0xffffffff;
|
|
107
|
-
for (let i = 0; i < data.length; ++i) crc = (crc >>> 8) ^ Crc32Table[(crc ^ data[i]) & 0xff];
|
|
108
|
-
return ~crc;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Create a ZIP blob from an array of {name: string, data: Uint8Array} entries.
|
|
112
|
-
// Uses stored method (no compression) with CRC-32 for maximum compatibility.
|
|
113
|
-
export function createZipBlob(files) {
|
|
114
|
-
const encoder = new TextEncoder();
|
|
115
|
-
const localHeaders = [];
|
|
116
|
-
const centralHeaders = [];
|
|
117
|
-
let offset = 0;
|
|
118
|
-
|
|
119
|
-
for (const { name, data } of files) {
|
|
120
|
-
const nameBytes = encoder.encode(name);
|
|
121
|
-
const crc = crc32(data);
|
|
122
|
-
// Local file header (30 + nameLen + data)
|
|
123
|
-
const local = new Uint8Array(30 + nameBytes.length + data.length);
|
|
124
|
-
const lv = new DataView(local.buffer);
|
|
125
|
-
lv.setUint32(0, 0x04034b50, true); // signature
|
|
126
|
-
lv.setUint16(4, 20, true); // version needed
|
|
127
|
-
lv.setUint16(8, 0, true); // method: stored
|
|
128
|
-
lv.setUint32(14, crc, true); // CRC-32
|
|
129
|
-
lv.setUint32(18, data.length, true); // compressed size
|
|
130
|
-
lv.setUint32(22, data.length, true); // uncompressed size
|
|
131
|
-
lv.setUint16(26, nameBytes.length, true); // name length
|
|
132
|
-
local.set(nameBytes, 30);
|
|
133
|
-
local.set(data, 30 + nameBytes.length);
|
|
134
|
-
localHeaders.push(local);
|
|
135
|
-
|
|
136
|
-
// Central directory entry (46 + nameLen)
|
|
137
|
-
const central = new Uint8Array(46 + nameBytes.length);
|
|
138
|
-
const cv = new DataView(central.buffer);
|
|
139
|
-
cv.setUint32(0, 0x02014b50, true); // signature
|
|
140
|
-
cv.setUint16(4, 20, true); // version made by
|
|
141
|
-
cv.setUint16(6, 20, true); // version needed
|
|
142
|
-
cv.setUint16(10, 0, true); // method: stored
|
|
143
|
-
cv.setUint32(16, crc, true); // CRC-32
|
|
144
|
-
cv.setUint32(20, data.length, true); // compressed size
|
|
145
|
-
cv.setUint32(24, data.length, true); // uncompressed size
|
|
146
|
-
cv.setUint16(28, nameBytes.length, true); // name length
|
|
147
|
-
cv.setUint32(42, offset, true); // local header offset
|
|
148
|
-
central.set(nameBytes, 46);
|
|
149
|
-
centralHeaders.push(central);
|
|
150
|
-
|
|
151
|
-
offset += local.length;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const cdOffset = offset;
|
|
155
|
-
let cdSize = 0;
|
|
156
|
-
for (const c of centralHeaders) cdSize += c.length;
|
|
157
|
-
|
|
158
|
-
// End of central directory (22 bytes)
|
|
159
|
-
const eocd = new Uint8Array(22);
|
|
160
|
-
const ev = new DataView(eocd.buffer);
|
|
161
|
-
ev.setUint32(0, 0x06054b50, true); // signature
|
|
162
|
-
ev.setUint16(8, files.length, true); // entries on this disc
|
|
163
|
-
ev.setUint16(10, files.length, true); // total entries
|
|
164
|
-
ev.setUint32(12, cdSize, true); // central directory size
|
|
165
|
-
ev.setUint32(16, cdOffset, true); // central directory offset
|
|
166
|
-
|
|
167
|
-
return new Blob([...localHeaders, ...centralHeaders, eocd], { type: "application/zip" });
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function debounce(fn, wait) {
|
|
171
|
-
let timeout;
|
|
172
|
-
return function (...args) {
|
|
173
|
-
clearTimeout(timeout);
|
|
174
|
-
timeout = setTimeout(() => fn.apply(this, args), wait);
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export const runningInNode = typeof window === "undefined";
|
|
1
|
+
import { runningInNode } from "./loader.js";
|
|
179
2
|
|
|
180
3
|
export function isFirefox() {
|
|
181
4
|
// With thanks to http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
|
@@ -183,12 +6,6 @@ export function isFirefox() {
|
|
|
183
6
|
return typeof InstallTrigger !== "undefined"; // Firefox 1.0+
|
|
184
7
|
}
|
|
185
8
|
|
|
186
|
-
export function parseAddr(s) {
|
|
187
|
-
if (s[0] === "$" || s[0] === "&") return parseInt(s.substring(1), 16);
|
|
188
|
-
if (s.indexOf("0x") === 0) return parseInt(s.substring(2), 16);
|
|
189
|
-
return parseInt(s, 16);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
9
|
export const userKeymap = [];
|
|
193
10
|
|
|
194
11
|
export const BBC = {
|
|
@@ -440,6 +257,13 @@ export function stringToBBCKeys(str) {
|
|
|
440
257
|
*/
|
|
441
258
|
export const keyCodes = {
|
|
442
259
|
UNDEFINED: 0,
|
|
260
|
+
SEMICOLON: 186,
|
|
261
|
+
HASH: 222,
|
|
262
|
+
APOSTROPHE: 192,
|
|
263
|
+
MUTE: 173,
|
|
264
|
+
MINUS: 189,
|
|
265
|
+
EQUALS: 187,
|
|
266
|
+
BACK_QUOTE: 223,
|
|
443
267
|
BACKSPACE: 8,
|
|
444
268
|
TAB: 9,
|
|
445
269
|
CLEAR: 12,
|
|
@@ -553,7 +377,7 @@ export const keyCodes = {
|
|
|
553
377
|
CTRL_RIGHT: 261, // hack, jsbeeb only
|
|
554
378
|
};
|
|
555
379
|
|
|
556
|
-
function detectKeyboardLayout() {
|
|
380
|
+
export function detectKeyboardLayout() {
|
|
557
381
|
if (runningInNode) {
|
|
558
382
|
return "UK";
|
|
559
383
|
}
|
|
@@ -567,41 +391,47 @@ function detectKeyboardLayout() {
|
|
|
567
391
|
return "UK"; // Default guess of UK
|
|
568
392
|
}
|
|
569
393
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
394
|
+
/**
|
|
395
|
+
* Adjusts keyCodes for the browser this runs in. The table starts as Chrome on a
|
|
396
|
+
* UK keyboard, which is also what node sees, so only the differences are applied.
|
|
397
|
+
*/
|
|
398
|
+
export function adaptKeyCodesToBrowser() {
|
|
399
|
+
const isUKlayout = detectKeyboardLayout() === "UK";
|
|
400
|
+
if (isFirefox()) {
|
|
401
|
+
keyCodes.SEMICOLON = 59;
|
|
402
|
+
// #~ key (not on US keyboard)
|
|
403
|
+
keyCodes.HASH = 163;
|
|
404
|
+
keyCodes.APOSTROPHE = 222;
|
|
405
|
+
keyCodes.BACK_QUOTE = 192;
|
|
406
|
+
// Firefox doesn't return a keycode for this
|
|
407
|
+
keyCodes.MUTE = -1;
|
|
408
|
+
keyCodes.MINUS = 173;
|
|
409
|
+
keyCodes.EQUALS = 61;
|
|
410
|
+
} else {
|
|
411
|
+
// Chrome
|
|
412
|
+
// TODO(#1065) check other browsers
|
|
413
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode
|
|
414
|
+
keyCodes.SEMICOLON = 186;
|
|
415
|
+
// #~ key (not on US keyboard)
|
|
416
|
+
keyCodes.HASH = isUKlayout ? 222 : 223;
|
|
417
|
+
keyCodes.APOSTROPHE = isUKlayout ? 192 : 222;
|
|
418
|
+
keyCodes.MUTE = 173;
|
|
419
|
+
keyCodes.MINUS = 189;
|
|
420
|
+
keyCodes.EQUALS = 187;
|
|
421
|
+
keyCodes.BACK_QUOTE = isUKlayout ? 223 : 192;
|
|
422
|
+
}
|
|
595
423
|
|
|
596
|
-
// Swap APOSTROPHE and BACK_QUOTE keys around for Mac users. They are the opposite to what jsbeeb expects.
|
|
597
|
-
// Swap them to what jsbeeb expects, and tidy up the hash key to prevent duplicate key mappings.
|
|
598
|
-
if (!runningInNode &&
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
424
|
+
// Swap APOSTROPHE and BACK_QUOTE keys around for Mac users. They are the opposite to what jsbeeb expects.
|
|
425
|
+
// Swap them to what jsbeeb expects, and tidy up the hash key to prevent duplicate key mappings.
|
|
426
|
+
if (!runningInNode && navigator.userAgent.indexOf("Mac") !== -1) {
|
|
427
|
+
keyCodes.BACK_QUOTE = 192;
|
|
428
|
+
keyCodes.APOSTROPHE = 222;
|
|
429
|
+
keyCodes.HASH = 223;
|
|
430
|
+
}
|
|
602
431
|
}
|
|
603
432
|
|
|
604
433
|
export function getKeyMap(keyLayout) {
|
|
434
|
+
const isUKlayout = detectKeyboardLayout() === "UK";
|
|
605
435
|
const keys2 = [];
|
|
606
436
|
|
|
607
437
|
// shift pressed
|
|
@@ -911,7 +741,7 @@ export function getKeyMap(keyLayout) {
|
|
|
911
741
|
// no keycode for NUMPADENTER, small hack in main.js/keyCode()
|
|
912
742
|
map(keyCodes.NUMPADENTER, BBC.NUMPADENTER);
|
|
913
743
|
|
|
914
|
-
// TODO
|
|
744
|
+
// TODO(#748) "game" mapping
|
|
915
745
|
// eg Master Dunjunz needs # Del 3 , * Enter
|
|
916
746
|
// https://web.archive.org/web/20080305042238/http://bbc.nvg.org/doc/games/Dunjunz-docs.txt
|
|
917
747
|
|
|
@@ -923,396 +753,3 @@ export function getKeyMap(keyLayout) {
|
|
|
923
753
|
|
|
924
754
|
return keys2;
|
|
925
755
|
}
|
|
926
|
-
|
|
927
|
-
export function replaceOrAddExtension(name, newExt) {
|
|
928
|
-
const lastDot = name.lastIndexOf(".");
|
|
929
|
-
if (lastDot === -1) {
|
|
930
|
-
return name + newExt;
|
|
931
|
-
}
|
|
932
|
-
return name.substring(0, lastDot) + newExt;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
export function hexbyte(value) {
|
|
936
|
-
return ((value >>> 4) & 0xf).toString(16) + (value & 0xf).toString(16);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
export function hexword(value) {
|
|
940
|
-
return hexbyte(value >>> 8) + hexbyte(value & 0xff);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
export function hd(reader, start, end, opts) {
|
|
944
|
-
opts = opts || {};
|
|
945
|
-
const width = opts.width || 16;
|
|
946
|
-
const gap = opts.gap === undefined ? 8 : opts.gap;
|
|
947
|
-
const res = [];
|
|
948
|
-
let str = "";
|
|
949
|
-
let j = 0;
|
|
950
|
-
for (let i = start; i < end; ++i) {
|
|
951
|
-
str += " ";
|
|
952
|
-
str += hexbyte(reader(i));
|
|
953
|
-
if (++j === gap) str += " ";
|
|
954
|
-
if (j === width) {
|
|
955
|
-
res.push(str);
|
|
956
|
-
str = "";
|
|
957
|
-
j = 0;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
if (str) res.push(str);
|
|
961
|
-
let joined = "";
|
|
962
|
-
for (let i = 0; i < res.length; ++i) {
|
|
963
|
-
joined += hexword(start + i * width) + " :" + res[i] + "\n";
|
|
964
|
-
}
|
|
965
|
-
return joined;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
export function signExtend(val) {
|
|
969
|
-
return val >= 128 ? val - 256 : val;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
export function noop() {}
|
|
973
|
-
|
|
974
|
-
export function bench() {
|
|
975
|
-
for (let j = 0; j < 10; ++j) {
|
|
976
|
-
let res = 0;
|
|
977
|
-
const start = Date.now();
|
|
978
|
-
for (let i = 0; i < 4096 * 1024; ++i) {
|
|
979
|
-
res += signExtend(i & 0xff);
|
|
980
|
-
}
|
|
981
|
-
const tt = Date.now() - start;
|
|
982
|
-
console.log(res, tt);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
export function noteEvent(category, type, label) {
|
|
987
|
-
if (
|
|
988
|
-
!runningInNode &&
|
|
989
|
-
(window.location.host.endsWith(".godbolt.org") || window.location.host.endsWith(".xania.org"))
|
|
990
|
-
) {
|
|
991
|
-
// Only note events on the public site
|
|
992
|
-
/*global gtag*/
|
|
993
|
-
gtag("event", category, { type, label });
|
|
994
|
-
}
|
|
995
|
-
console.log("event noted:", category, type, label);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
let baseUrl = "";
|
|
999
|
-
|
|
1000
|
-
export function setBaseUrl(url) {
|
|
1001
|
-
baseUrl = url;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
export function uint8ArrayToString(array) {
|
|
1005
|
-
let str = "";
|
|
1006
|
-
for (let i = 0; i < array.length; ++i) str += String.fromCharCode(array[i]);
|
|
1007
|
-
return str;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
export function stringToUint8Array(str) {
|
|
1011
|
-
if (str instanceof Uint8Array) return str;
|
|
1012
|
-
const len = str.length;
|
|
1013
|
-
const array = new Uint8Array(len);
|
|
1014
|
-
for (let i = 0; i < len; ++i) array[i] = str.charCodeAt(i) & 0xff;
|
|
1015
|
-
return array;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function loadDataHttp(url) {
|
|
1019
|
-
return new Promise(function (resolve, reject) {
|
|
1020
|
-
const request = new XMLHttpRequest();
|
|
1021
|
-
request.open("GET", baseUrl + url, true);
|
|
1022
|
-
request.overrideMimeType("text/plain; charset=x-user-defined");
|
|
1023
|
-
request.onload = function () {
|
|
1024
|
-
if (request.status !== 200) {
|
|
1025
|
-
reject(new Error("Unable to load " + url + ", http code " + request.status));
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
if (typeof request.response !== "string") {
|
|
1029
|
-
resolve(request.response);
|
|
1030
|
-
} else {
|
|
1031
|
-
resolve(stringToUint8Array(request.response));
|
|
1032
|
-
}
|
|
1033
|
-
};
|
|
1034
|
-
request.onerror = function () {
|
|
1035
|
-
reject(new Error("A network error occurred loading " + url));
|
|
1036
|
-
};
|
|
1037
|
-
request.send(null);
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
let _nodeBasePath = null;
|
|
1042
|
-
|
|
1043
|
-
export function setNodeBasePath(basePath) {
|
|
1044
|
-
_nodeBasePath = basePath;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
async function loadDataNode(url) {
|
|
1048
|
-
if (typeof readbuffer !== "undefined") {
|
|
1049
|
-
// d8 shell
|
|
1050
|
-
/*global readbuffer*/
|
|
1051
|
-
return new Uint8Array(readbuffer(url));
|
|
1052
|
-
} else if (typeof read !== "undefined") {
|
|
1053
|
-
// SpiderMonkey shell
|
|
1054
|
-
/*global read*/
|
|
1055
|
-
return read(url, "binary");
|
|
1056
|
-
} else {
|
|
1057
|
-
// Node
|
|
1058
|
-
const fs = await import("fs");
|
|
1059
|
-
const nodePath = await import("path");
|
|
1060
|
-
if (_nodeBasePath) {
|
|
1061
|
-
const publicPath = nodePath.join(_nodeBasePath, "public", url);
|
|
1062
|
-
if (fs.existsSync(publicPath)) return fs.readFileSync(publicPath);
|
|
1063
|
-
return fs.readFileSync(nodePath.join(_nodeBasePath, url));
|
|
1064
|
-
}
|
|
1065
|
-
if (url[0] === "/") url = "." + url;
|
|
1066
|
-
if (fs.existsSync("public/" + url)) return fs.readFileSync("public/" + url);
|
|
1067
|
-
return fs.readFileSync(url);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
export function loadData(url) {
|
|
1072
|
-
if (runningInNode) {
|
|
1073
|
-
return loadDataNode(url);
|
|
1074
|
-
} else {
|
|
1075
|
-
return loadDataHttp(url);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
export function readInt32(data, offset) {
|
|
1080
|
-
return (data[offset + 3] << 24) | (data[offset + 2] << 16) | (data[offset + 1] << 8) | data[offset + 0];
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
export function readInt16(data, offset) {
|
|
1084
|
-
return (data[offset + 1] << 8) | data[offset + 0];
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const tempBuf = new ArrayBuffer(4);
|
|
1088
|
-
const tempBuf8 = new Uint8Array(tempBuf);
|
|
1089
|
-
const tempBufF32 = new Float32Array(tempBuf);
|
|
1090
|
-
|
|
1091
|
-
export function readFloat32(data, offset) {
|
|
1092
|
-
tempBuf8[0] = data[offset];
|
|
1093
|
-
tempBuf8[1] = data[offset + 1];
|
|
1094
|
-
tempBuf8[2] = data[offset + 2];
|
|
1095
|
-
tempBuf8[3] = data[offset + 3];
|
|
1096
|
-
return tempBufF32[0];
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
export async function ungzip(data) {
|
|
1100
|
-
return inflateWith(pakoUngzip, data, "Unable to ungzip");
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
export class DataStream {
|
|
1104
|
-
constructor(name, data) {
|
|
1105
|
-
this.name = name;
|
|
1106
|
-
this.pos = 0;
|
|
1107
|
-
this.data = stringToUint8Array(data);
|
|
1108
|
-
if (!this.data) {
|
|
1109
|
-
throw new Error("No data in " + name);
|
|
1110
|
-
}
|
|
1111
|
-
this.end = this.data.length;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
static async create(name, data) {
|
|
1115
|
-
const raw = stringToUint8Array(data);
|
|
1116
|
-
if (raw && raw.length > 4 && raw[0] === 0x1f && raw[1] === 0x8b) {
|
|
1117
|
-
console.log("Ungzipping " + name);
|
|
1118
|
-
data = await ungzip(raw);
|
|
1119
|
-
}
|
|
1120
|
-
return new DataStream(name, data);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
bytesLeft() {
|
|
1124
|
-
return this.end - this.pos;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
eof() {
|
|
1128
|
-
return this.bytesLeft() === 0;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
advance(distance) {
|
|
1132
|
-
if (this.bytesLeft() < distance) throw new RangeError("EOF in " + this.name);
|
|
1133
|
-
this.pos += distance;
|
|
1134
|
-
return this.pos - distance;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
readFloat32(pos) {
|
|
1138
|
-
if (pos === undefined) pos = this.advance(4);
|
|
1139
|
-
return readFloat32(this.data, pos);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
readInt32(pos) {
|
|
1143
|
-
if (pos === undefined) pos = this.advance(4);
|
|
1144
|
-
return readInt32(this.data, pos);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
readInt16(pos) {
|
|
1148
|
-
if (pos === undefined) pos = this.advance(2);
|
|
1149
|
-
return readInt16(this.data, pos);
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
readByte(pos) {
|
|
1153
|
-
if (pos === undefined) pos = this.advance(1);
|
|
1154
|
-
return this.data[pos];
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
readNulString(pos, maxLength) {
|
|
1158
|
-
if (!maxLength) maxLength = 1024;
|
|
1159
|
-
let posToUse = pos === undefined ? this.pos : pos;
|
|
1160
|
-
let result = "";
|
|
1161
|
-
let c;
|
|
1162
|
-
while ((c = this.readByte(posToUse++)) !== 0 && --maxLength) {
|
|
1163
|
-
result += String.fromCharCode(c);
|
|
1164
|
-
}
|
|
1165
|
-
if (maxLength === 0) return "";
|
|
1166
|
-
if (pos === undefined) this.pos = posToUse;
|
|
1167
|
-
return result;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
substream(posOrLength, length) {
|
|
1171
|
-
let pos;
|
|
1172
|
-
if (length === undefined) {
|
|
1173
|
-
length = posOrLength;
|
|
1174
|
-
pos = this.advance(length);
|
|
1175
|
-
} else {
|
|
1176
|
-
pos = posOrLength;
|
|
1177
|
-
if (pos + length >= this.end) throw new RangeError("EOF in " + this.name);
|
|
1178
|
-
}
|
|
1179
|
-
return new DataStream(this.name + ".sub", this.data.subarray(pos, pos + length));
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
seek(to) {
|
|
1183
|
-
if (to >= this.end) throw new RangeError("Seek out of range in " + this.name);
|
|
1184
|
-
this.pos = to;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
export function makeFast32(u32) {
|
|
1189
|
-
// Firefox is ~5% faster with signed 32-bit arrays. Chrome is the same speed
|
|
1190
|
-
// either way, so here we unconditionally wrap all u32 buffers as i32.
|
|
1191
|
-
// Having a function do this makes it easy to test u32 vs i32, and means we
|
|
1192
|
-
// keep the rest of the code using u32 (which makes more sense to me).
|
|
1193
|
-
return new Int32Array(u32.buffer);
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
const knownDiscExtensions = {
|
|
1197
|
-
uef: true,
|
|
1198
|
-
ssd: true,
|
|
1199
|
-
dsd: true,
|
|
1200
|
-
adf: true,
|
|
1201
|
-
adm: true,
|
|
1202
|
-
adl: true,
|
|
1203
|
-
hfe: true,
|
|
1204
|
-
};
|
|
1205
|
-
|
|
1206
|
-
const knownRomExtensions = {
|
|
1207
|
-
rom: true,
|
|
1208
|
-
};
|
|
1209
|
-
|
|
1210
|
-
async function unzipImage(data, knownExtensions) {
|
|
1211
|
-
console.log("Attempting to unzip");
|
|
1212
|
-
|
|
1213
|
-
let files;
|
|
1214
|
-
try {
|
|
1215
|
-
files = await unzip(data);
|
|
1216
|
-
} catch (e) {
|
|
1217
|
-
throw new Error("Error unzipping " + e.message, { cause: e });
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
let uncompressed = null;
|
|
1221
|
-
let loadedFile;
|
|
1222
|
-
const ignored = [];
|
|
1223
|
-
|
|
1224
|
-
for (const [filename, fileData] of Object.entries(files)) {
|
|
1225
|
-
const match = filename.match(/.*\.([a-z]+)/i);
|
|
1226
|
-
if (!match || !knownExtensions[match[1].toLowerCase()]) {
|
|
1227
|
-
console.log("Skipping file", filename);
|
|
1228
|
-
continue;
|
|
1229
|
-
}
|
|
1230
|
-
if (uncompressed) {
|
|
1231
|
-
console.log("Ignoring", filename, "as already found a file");
|
|
1232
|
-
ignored.push(filename);
|
|
1233
|
-
continue;
|
|
1234
|
-
}
|
|
1235
|
-
loadedFile = filename;
|
|
1236
|
-
uncompressed = fileData;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
if (!uncompressed) {
|
|
1240
|
-
throw new Error("Couldn't find any compatible files in the archive");
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
console.log("Unzipped '" + loadedFile + "'");
|
|
1244
|
-
return { data: uncompressed, name: loadedFile, ignored };
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
/**
|
|
1248
|
-
* @param {Uint8Array|number[]} data a ZIP archive
|
|
1249
|
-
* @returns {Promise<{data: Uint8Array, name: string, ignored: string[]}>} the one file loaded, and the
|
|
1250
|
-
* names of any other loadable files the archive held
|
|
1251
|
-
*/
|
|
1252
|
-
export async function unzipDiscImage(data) {
|
|
1253
|
-
return unzipImage(data, knownDiscExtensions);
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
export async function unzipRomImage(data) {
|
|
1257
|
-
return unzipImage(data, knownRomExtensions);
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
export function resizeUint8Array(array, byteSize) {
|
|
1261
|
-
const newArray = new Uint8Array(byteSize);
|
|
1262
|
-
newArray.set(array.subarray(0, byteSize));
|
|
1263
|
-
return newArray;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
export class Fifo {
|
|
1267
|
-
constructor(capacity) {
|
|
1268
|
-
this._buffer = new Uint8Array(capacity);
|
|
1269
|
-
this._size = 0;
|
|
1270
|
-
this._wPtr = 0;
|
|
1271
|
-
this._rPtr = 0;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
/** @returns {number} */
|
|
1275
|
-
get size() {
|
|
1276
|
-
return this._size;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
/** @returns {boolean} */
|
|
1280
|
-
get full() {
|
|
1281
|
-
return this._size === this._buffer.length;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
/** @returns {boolean} */
|
|
1285
|
-
get empty() {
|
|
1286
|
-
return this._size === 0;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
clear() {
|
|
1290
|
-
this._size = 0;
|
|
1291
|
-
this._wPtr = 0;
|
|
1292
|
-
this._rPtr = 0;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
/** @type {Number} b */
|
|
1296
|
-
put(b) {
|
|
1297
|
-
if (this.full) return;
|
|
1298
|
-
this._buffer[this._wPtr % this._buffer.length] = b;
|
|
1299
|
-
this._wPtr++;
|
|
1300
|
-
this._size++;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
/** @returns {Number} */
|
|
1304
|
-
get() {
|
|
1305
|
-
if (this.empty) return;
|
|
1306
|
-
const res = this._buffer[this._rPtr % this._buffer.length];
|
|
1307
|
-
this._rPtr++;
|
|
1308
|
-
this._size--;
|
|
1309
|
-
return res;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
/** @returns {number[]} pending bytes, oldest first */
|
|
1313
|
-
toArray() {
|
|
1314
|
-
const result = [];
|
|
1315
|
-
for (let i = 0; i < this._size; ++i) result.push(this._buffer[(this._rPtr + i) % this._buffer.length]);
|
|
1316
|
-
return result;
|
|
1317
|
-
}
|
|
1318
|
-
}
|