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