OXYGENHUB
# 0adc2e74f5cf x16-emulator / webassembly / main.js
20.6 KiB · JavaScript · last changed chore(): initial commit 15 days ago source blame history raw
1
2
// DOM elements
3
const statusElement = document.getElementById('status');
4
const output = document.getElementById('output');
5
const canvas = document.getElementById('canvas');
6
const code = document.getElementById('code');
7
const progressElement = document.getElementById('progress');
8
const spinnerElement = document.getElementById('spinner');
9
const volumeElementFullScreen = document.getElementById('fullscreen_volume_icon');
10
const volumeElement = document.getElementById('volume_icon');
11
12
13
// Audio Context Setup
14
var audioContext;
15
16
window.addEventListener('load', init, false);
17
function init() {
18
19
    try {
20
        window.AudioContext = window.AudioContext || window.webkitAudioContext;
21
        audioContext = new AudioContext();
22
    }
23
    catch (e) {
24
        console.log("AudioContext not supported on this Browser.")
25
    }
26
}
27
28
//detecting keyboard layout...
29
30
//define valid layouts (this can be gotten by running the emulator with -keymap)
31
const layouts = [
32
    'en-us',
33
    'en-us-int',
34
    'en-gb',
35
    'sv',
36
    'de',
37
    'da',
38
    'it',
39
    'pl',
40
    'nb',
41
    'hu',
42
    'es',
43
    'fi',
44
    'pt-br',
45
    'cz',
46
    'jp',
47
    'fr',
48
    'de-ch',
49
    'en-us-dvo',
50
    'et',
51
    'fr-be',
52
    'fr-ca',
53
    'is',
54
    'pt',
55
    'hr',
56
    'sk',
57
    'sl',
58
    'lv',
59
    'lt'
60
];
61
62
lang = getFirstBrowserLanguage().toLowerCase().trim();
63
64
if (layouts.includes(lang)) {
65
    logOutput('Using keyboard map: ' + lang);
66
} else {
67
    logOutput('Language (' + lang + ') not found in keymaps so using keyboard map: en-us');
68
    lang = 'en-us';
69
}
70
71
var url = new URL(window.location.href);
72
var manifest_link = url.searchParams.get("manifest");
73
var ram_val = url.searchParams.get("ram");
74
var cpu_val = url.searchParams.get("cpu");
75
var mhz_val = url.searchParams.get("mhz");
76
var lang_val = url.searchParams.get("keymap");
77
var diag = url.searchParams.get("longpwron");
78
var wide = url.searchParams.get("widescreen");
79
var capture = url.searchParams.get("capture");
80
var midline = url.searchParams.get("midlineeffects");
81
82
var emuArguments = ['-keymap', lang, '-rtc'];
83
84
if (ram_val) {
85
    emuArguments.push('-ram', ram_val);
86
}
87
if (cpu_val) {
88
    if (cpu_val == 'c816') {
89
        emuArguments.push('-c816');
90
    } else if (cpu_val == 'c02') {
91
	emuArguments.push('-c02');
92
    }
93
}
94
if (mhz_val) {
95
    emuArguments.push('-mhz', mhz_val);
96
}
97
if (lang_val) {
98
    emuArguments.push('-keymap', lang_val);
99
}
100
if (diag != null) {
101
    emuArguments.push('-longpwron');
102
}
103
if (wide != null) {
104
    emuArguments.push('-widescreen');
105
}
106
if (capture != null) {
107
    emuArguments.push('-capture');
108
}
109
if (midline != null) {
110
    emuArguments.push('-midline-effects');
111
}
112
113
if (manifest_link) {
114
    openFs();
115
}
116
117
var Module = {
118
119
    preRun: [
120
        function () { //Set the keyboard handling element (it's document by default). Keystrokes are stopped from propagating by emscripten, maybe there's an option to disable this?
121
            ENV.SDL_EMSCRIPTEN_KEYBOARD_ELEMENT = "#canvas";
122
        },
123
        function () {
124
125
            if (manifest_link) {
126
                loadManifestLink();
127
            }
128
        }
129
    ],
130
    postRun: [
131
        function () {
132
            canvas.focus();
133
        }
134
    ],
135
    arguments: emuArguments,
136
    print: (function () {
137
138
        if (output) output.value = ''; // clear browser cache
139
        return function (text) {
140
            if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
141
            logOutput(text);
142
        };
143
    })(),
144
    printErr: function (text) {
145
        if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
146
147
        // filtering out some known issues for easier reporting from people who have startup problems
148
        if (text.startsWith('wasm streaming compile failed:') ||
149
            text.startsWith('falling back to ArrayBuffer instantiation') ||
150
            text.startsWith('Calling stub instead of sigaction')) {
151
            logOutput("[known behavior] " + text);
152
            return;
153
        }
154
155
        logError(text);
156
157
158
    },
159
    canvas: (function () {
160
161
        // As a default initial behavior, pop up an alert when webgl context is lost. To make your
162
        // application robust, you may want to override this behavior before shipping!
163
        // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
164
        canvas.addEventListener("webglcontextlost", function (e) {
165
            alert('WebGL context lost. You will need to reload the page.');
166
            e.preventDefault();
167
        }, false);
168
        return canvas;
169
    })(),
170
    setStatus: function (text) {
171
        if (!Module.setStatus.last) Module.setStatus.last = {
172
            time: Date.now(),
173
            text: ''
174
        };
175
        if (text === Module.setStatus.last.text) return;
176
        const m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
177
        let now = Date.now();
178
        if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
179
        Module.setStatus.last.time = now;
180
        Module.setStatus.last.text = text;
181
        if (m) {
182
            text = m[1];
183
            progressElement.value = parseInt(m[2]) * 100;
184
            progressElement.max = parseInt(m[4]) * 100;
185
            progressElement.hidden = false;
186
            spinnerElement.hidden = false;
187
        } else {
188
            progressElement.value = null;
189
            progressElement.max = null;
190
            progressElement.hidden = true;
191
            if (!text) spinnerElement.hidden = true;
192
        }
193
        statusElement.innerHTML = text;
194
        logOutput(text);
195
    },
196
    totalDependencies: 0,
197
    monitorRunDependencies: function (left) {
198
        this.totalDependencies = Math.max(this.totalDependencies, left);
199
        Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
200
    }
201
};
202
203
Module.setStatus('Downloading file...');
204
logOutput('Downloading file...');
205
206
window.onerror = function () {
207
    // Module.setStatus('Exception thrown, see JavaScript console');
208
    spinnerElement.style.display = 'none';
209
    Module.setStatus = function (text) {
210
        if (text) Module.printErr('[post-exception status] ' + text);
211
    };
212
};
213
214
function loadManifestLink() {
215
    addRunDependency('load-manifest-link');
216
    console.log("Loading URL:", manifest_link);
217
    fetch(manifest_link)
218
        .then(function (response) {
219
            var disposition = response.headers.get('Content-Disposition');
220
            if (disposition) {
221
                var filename = parseDispositionFilename(disposition);
222
                if (filename) {
223
                    console.log("Loading filename:", filename);
224
                    if (filename.toLowerCase().endsWith('.bas')) {
225
                        console.log("Loading from BAS.");
226
                        loadBas(manifest_link, filename);
227
                    } else if (filename.toLowerCase().endsWith('.prg')) {
228
                        console.log("Loading from PRG.");
229
                        loadPrg(manifest_link, filename);
230
                    } else if (filename.toLowerCase().endsWith('.zip')) {
231
                        console.log("Loading from ZIP.");
232
                        loadZip(manifest_link);
233
                    }
234
                }
235
            } else {
236
                if (manifest_link.toLowerCase().endsWith('.bas')) {
237
                    var filename = manifest_link.replace(/^.*[\\\/]/, '');
238
                    console.log("Loading from BAS.");
239
                    loadBas(manifest_link, filename);
240
                } else if (manifest_link.toLowerCase().endsWith('.prg')) {
241
                    var filename = manifest_link.replace(/^.*[\\\/]/, '');
242
                    console.log("Loading from PRG.");
243
                    loadPrg(manifest_link, filename);
244
                } else if (manifest_link.toLowerCase().endsWith('.zip')) {
245
                    console.log("Loading from ZIP.");
246
                    loadZip(manifest_link);
247
                } else {
248
                    console.log("Loading from directory.");
249
                    if (!manifest_link.endsWith('/')) {
250
                        manifest_link = manifest_link + '/';
251
                    }
252
                    loadManifest();
253
                }
254
            }
255
        })
256
        .then(function () {
257
            removeRunDependency('load-manifest-link');
258
        });
259
}
260
261
function parseDispositionFilename(disposition) {
262
    const utf8FilenameRegex = /filename\*=UTF-8''([\w%\-\.]+)(?:; ?|$)/i;
263
    const asciiFilenameRegex = /^filename=(["']?)(.*?[^\\])\1(?:; ?|$)/i;
264
265
    let fileName = null;
266
    if (utf8FilenameRegex.test(disposition)) {
267
        fileName = decodeURIComponent(utf8FilenameRegex.exec(disposition)[1]);
268
    } else {
269
        // prevent ReDos attacks by anchoring the ascii regex to string start and
270
        // slicing off everything before 'filename='
271
        const filenameStart = disposition.toLowerCase().indexOf('filename=');
272
        if (filenameStart >= 0) {
273
            const partialDisposition = disposition.slice(filenameStart);
274
            const matches = asciiFilenameRegex.exec(partialDisposition);
275
            if (matches != null && matches[2]) {
276
                fileName = matches[2];
277
            }
278
        }
279
    }
280
    return fileName;
281
}
282
283
function loadBas(basFileUrl, filename) {
284
    console.log('Adding start BAS:', filename)
285
    emuArguments.push('-bas', filename, '-run');
286
    FS.createPreloadedFile('/', filename, basFileUrl, true, true);
287
    console.log("Starting Emulator...")
288
    console.log("Emulator arguments: ", emuArguments)
289
}
290
291
function loadPrg(prgFileUrl, filename) {
292
    console.log('Adding start PRG:', filename)
293
    emuArguments.push('-prg', filename, '-run');
294
    FS.createPreloadedFile('/', filename, prgFileUrl, true, true);
295
    console.log("Starting Emulator...")
296
    console.log("Emulator arguments: ", emuArguments)
297
}
298
299
function loadZip(zipFileUrl) {
300
    addRunDependency('load-zip');
301
    fetch(zipFileUrl)
302
        .then(function (response) {
303
            if (response.status === 200 || response.status === 0) {
304
                return Promise.resolve(response.blob());
305
            } else {
306
                return Promise.reject(new Error(response.statusText));
307
                // todo error handling here, display to user
308
            }
309
        })
310
        .then(JSZip.loadAsync)
311
        .then(extractManifestFromBuffer)
312
        .then(function () {
313
            console.log("Starting Emulator...")
314
            console.log("Emulator arguments: ", emuArguments)
315
            removeRunDependency('load-zip');
316
        });
317
}
318
319
function extractManifestFromBuffer(zip) {
320
    if (zip.file("manifest.json") == null) {
321
        console.log("Unable to find manifest.json file. Writing all files from zip.");
322
        const promises = [];
323
        writeAllFilesFromZip(zip, promises, []);
324
        return Promise.all(promises).then((value) => {
325
            console.log("Emulator filesystem loading complete.")
326
        });
327
    }
328
    else {
329
        return zip.file("manifest.json").async("uint8array")
330
            .then(function (content) {
331
                let manifestString = new TextDecoder("utf-8").decode(content);
332
                let manifestObject = JSON.parse(manifestString);
333
                console.log("Parsed manifest from zip:")
334
                console.log(manifestObject);
335
336
                if (manifestObject.ram) {
337
                    console.log('Found RAM amount: '+manifestObject.ram);
338
                    emuArguments.push('-ram', manifestObject.ram);
339
                }
340
341
                let gen1 = true;
342
343
                if (manifestObject.model) {
344
                    console.log('Found model: '+manifestObject.model);
345
                    if (manifestObject.model === 'gs') {
346
                        gen1 = false;
347
                        emuArguments.push('-gs');
348
                    }
349
                }
350
351
                if (gen1 === true && manifestObject.cpu) {
352
                    console.log('Found CPU type: '+manifestObject.cpu);
353
                    if (manifestObject.cpu == 'c816') {
354
                        emuArguments.push('-c816');
355
                    } else if (manifestObject.cpu == 'c02') {
356
                        emuArguments.push('-c02');
357
                    }
358
                }
359
                if (manifestObject.mhz) {
360
                    console.log('Found mhz variable: '+manifestObject.mhz);
361
                    emuArguments.push('-mhz', manifestObject.mhz);
362
                }
363
                if (manifestObject.keymap) {
364
                    console.log('Found keymap variable: '+manifestObject.keymap);
365
                    emuArguments.push('-keymap', manifestObject.keymap);
366
                }
367
                if (manifestObject.longpwron) {
368
                    console.log('Found longpwron variable');
369
                    emuArguments.push('-longpwron');
370
                }
371
                if (manifestObject.widescreen) {
372
                    console.log('Found widescreen variable');
373
                    emuArguments.push('-widescreen');
374
                }
375
                if (manifestObject.capture) {
376
                    console.log('Found capture variable');
377
                    emuArguments.push('-capture');
378
                }
379
                if (manifestObject.midlineeffects) {
380
                    console.log('Found midlineeffects variable');
381
                    emuArguments.push('-midline-effects');
382
                }
383
384
                const promises = [];
385
                if (manifestObject.resources) {
386
                    console.log('Found resources section in manifest.');
387
                    var startFiles = [];
388
                    manifestObject.resources.forEach(function (element) {
389
                        let fileName = element.replace(/^.*[\\\/]/, '');
390
                        if (fileName.toLowerCase().endsWith(".bas") || fileName.toLowerCase().endsWith(".prg")) {
391
                            startFiles.push(fileName);
392
                        }
393
                        if (zip.file(fileName) == null) {
394
                            logError("Unable to find resources entry: " + fileName);
395
                            logError("This is likely an error, check resources section in manifest.")
396
                        } else {
397
                            promises.push(zip.file(fileName).async("uint8array").then(function (content) {
398
                                console.log('Writing to emulator filesystem:', fileName);
399
                                try {
400
                                    FS.writeFile(fileName, content);
401
                                }
402
                                catch(e) {
403
                                    console.log('Error writing to emulator filesystem:', file.name);
404
                                }
405
                            }));
406
                        }
407
                        addStartFile(manifestObject, startFiles);
408
                    });
409
                } else {
410
                    console.log('Resources section not found in manifest. Writing all files from zip.');
411
                    writeAllFilesFromZip(zip, promises, manifestObject);
412
                }
413
                return Promise.all(promises);
414
            })
415
            .then((value) => {
416
                console.log("Emulator filesystem loading complete.")
417
            });
418
    }
419
}
420
421
function writeAllFilesFromZip(zip, promises, manifestObject) {
422
    var startFiles = [];
423
    const writeResources = (zip) => {
424
        zip.forEach((path, file) => {
425
            if(file.dir) {
426
                FS.mkdirTree(file.name);
427
                writeResources(zip.folder(path));
428
                return;
429
            } else {
430
                if (file.name.toLowerCase().endsWith(".bas") || file.name.toLowerCase().endsWith(".prg")) {
431
                    startFiles.push(file.name);
432
                }
433
            }
434
435
            promises.push(zip.file(path).async("uint8array")
436
                .then(function (content) {
437
                    console.log('Writing to emulator filesystem:', file.name);
438
                    try {
439
                        FS.writeFile(file.name, content);
440
                    }
441
                    catch(e) {
442
                        console.log('Error writing to emulator filesystem:', file.name);
443
                    }
444
                })
445
            );
446
        });
447
    };
448
    writeResources(zip);
449
    addStartFile(manifestObject, startFiles);
450
}
451
452
function loadManifest() {
453
    addRunDependency('load-manifest');
454
    fetch(manifest_link + 'manifest.json').then(function (response) {
455
        return response.json();
456
    }).then(function (manifest) {
457
        console.log("Loading from manifest:")
458
        console.log(manifest);
459
        var startFiles = [];
460
        manifest.resources.forEach(element => {
461
            element = manifest_link + element;
462
            let filename = element.replace(/^.*[\\\/]/, '')
463
            if (filename.toLowerCase().endsWith(".bas") || filename.toLowerCase().endsWith(".prg")) {
464
                startFiles.push(filename);
465
            }
466
            FS.createPreloadedFile('/', filename, element, true, true);
467
        });
468
        addStartFile(manifest, startFiles);
469
        console.log("Starting Emulator...")
470
        console.log("Emulator arguments: ", emuArguments)
471
        removeRunDependency('load-manifest');
472
    }).catch(function () {
473
        console.log("Unable to read manifest. Check the manifest http parameter");
474
    });
475
}
476
477
function addStartFile(manifestObject, startFiles) {
478
    if (manifestObject && manifestObject.start_bas && manifestObject.start_prg) {
479
        logError("start_bas and start_prg used in manifest");
480
        logError("This is likely an error, defaulting to start_bas")
481
    }
482
483
    if (manifestObject && manifestObject.start_bas) {
484
        console.log('Adding start BAS:', manifestObject.start_bas)
485
        emuArguments.push('-bas', manifestObject.start_bas, '-run');
486
    } else if (manifestObject && manifestObject.start_prg) {
487
        console.log('Adding start PRG:', manifestObject.start_prg)
488
        emuArguments.push('-prg', manifestObject.start_prg, '-run');
489
    } else if (startFiles) {
490
        if (startFiles.length === 1) {
491
            // If there is a single BAS or PRG file, execute it.
492
            var filename = startFiles[0];
493
            if (filename.toLowerCase().endsWith(".bas")) {
494
                console.log('Adding start BAS:', filename)
495
                emuArguments.push('-bas', filename, '-run');
496
            } else if (filename.toLowerCase().endsWith(".prg")) {
497
                console.log('Adding start PRG:', filename)
498
                emuArguments.push('-prg', filename, '-run');
499
            }
500
        } else {
501
            logOutput("Start files: " + startFiles);
502
            // Let the user decide which file to LOAD.
503
        }
504
    }
505
}
506
507
function toggleAudio() {
508
    if (audioContext && audioContext.state != "running") {
509
        audioContext.resume().then(() => {
510
            volumeElement.innerHTML = "volume_up";
511
            volumeElementFullScreen.innerHTML = "volume_up";
512
            console.log("Resumed Audio.")
513
            Module.ccall("j2c_start_audio", "void", ["bool"], [true]);
514
        });
515
    } else if (audioContext && audioContext.state == "running") {
516
        audioContext.suspend().then(function () {
517
            console.log("Stopped Audio.")
518
            volumeElement.innerHTML = "volume_off";
519
            volumeElementFullScreen.innerHTML = "volume_off";
520
            Module.ccall("j2c_start_audio", "void", ["bool"], [false]);
521
        });
522
    }
523
    canvas.focus();
524
}
525
526
function resetEmulator() {
527
    j2c_reset = Module.cwrap("j2c_reset", "void", []);
528
    j2c_reset();
529
    canvas.focus();
530
}
531
532
function runCode() {
533
    Module.ccall("j2c_paste", "void", ["string"], ['\nNEW\n' + code.value + '\nRUN\n']);
534
    canvas.focus();
535
}
536
537
function closeFs() {
538
    canvas.parentElement.classList.remove("fullscreen");
539
    canvas.focus();
540
}
541
542
function openFs() {
543
    canvas.parentElement.classList.add("fullscreen");
544
    canvas.focus();
545
}
546
547
function logOutput(text) {
548
    if (output) {
549
        output.innerHTML += text + "\n";
550
        output.parentElement.scrollTop = output.parentElement.scrollHeight; // focus on bottom
551
    }
552
    console.log(text);
553
}
554
555
function logError(text) {
556
    if (output) {
557
        output.innerHTML += "[error] " + text + "\n";
558
        output.parentElement.scrollTop = output.parentElement.scrollHeight; // focus on bottom
559
    }
560
    console.error(text);
561
}
562
563
564
565
function getFirstBrowserLanguage() {
566
    const nav = window.navigator,
567
        browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'];
568
    let i,
569
        language;
570
571
    // support for HTML 5.1 "navigator.languages"
572
    if (Array.isArray(nav.languages)) {
573
        for (i = 0; i < nav.languages.length; i++) {
574
            language = nav.languages[i];
575
            if (language && language.length) {
576
                return language;
577
            }
578
        }
579
    }
580
581
    // support for other well known properties in browsers
582
    for (i = 0; i < browserLanguagePropertyKeys.length; i++) {
583
        language = nav[browserLanguagePropertyKeys[i]];
584
        if (language && language.length) {
585
            return language;
586
        }
587
    }
588
589
    return null;
590
}