/** The Emularity; easily embed emulators * Copyright © 2014-2016 Daniel Brooks , Jason * Scott , Grant Galitz , * John Vilk , and Tracey Jaquith * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ var Module = null; (function (Promise) { /** * IALoader */ function IALoader(canvas, game, callbacks, scale) { // IA actually gives us an object here, and we really ought to be // looking things up from it instead. if (typeof game !== 'string') { game = game.toString(); } if (!callbacks || typeof callbacks !== 'object') { callbacks = { before_emulator: updateLogo, before_run: callbacks }; } else { if (typeof callbacks.before_emulator === 'function') { var func = callbacks.before_emulator; callbacks.before_emulator = function () { updateLogo(); func(); }; } else { callbacks.before_emulator = updateLogo; } } function img(src) { var img = new Image(); img.src = src; return img; } // yea, this is a hack var images; if (/archive\.org$/.test(document.location.hostname)) { images = { ia: img("/images/ialogo.png"), mame: img("/images/mame.png"), mess: img("/images/mame.png"), dosbox: img("/images/dosbox.png"), sae: img("/images/sae.png"), pce: img("/images/pce.png") }; } else { images = { ia: img("other_logos/ia-logo-150x150.png"), mame: img("other_logos/mame.png"), mess: img("other_logos/mame.png"), dosbox: img("other_logos/dosbox.png"), sae: img("other_logos/sae.png"), pce: img("other_logos/pce.png") }; } function updateLogo() { /* if (emulator_logo) { emulator.setSplashImage(emulator_logo); } */ } var SAMPLE_RATE = (function () { var audio_ctx = window.AudioContext || window.webkitAudioContext || false; if (!audio_ctx) { return false; } var sample = new audio_ctx; return sample.sampleRate.toString(); }()); var metadata, filelist, module, modulecfg, config_args, emulator_logo, emulator = new Emulator(canvas).setScale(scale) //.setSplashImage(images.ia) // BORA .setLoad(loadFiles) .setCallbacks(callbacks); var cfgr; function loadFiles(fetch_file, splash) { splash.setTitle("Downloading metadata..."); return new Promise(function (resolve, reject) { var loading = fetch_file('Metadata', get_meta_url(game), 'document'); loading.then(function (data) { metadata = data; splash.setTitle("Downloading file list..."); return fetch_file('File List', get_files_url(game), 'document', true); }, function () { splash.setTitle("Failed to download IA item metadata!"); splash.failed_loading = true; reject(1); }) .then(function (data) { if (splash.failed_loading) { return null; } filelist = data; splash.setTitle("Downloading emulator metadata..."); module = metadata.getElementsByTagName("emulator") .item(0) .textContent; return fetch_file('Emulator Metadata', get_emulator_config_url(module), 'text', true); }, function () { if (splash.failed_loading) { return; } splash.setTitle("Failed to download file list!"); splash.failed_loading = true; reject(2); }) .then(function (data) { if (splash.failed_loading) { return null; } modulecfg = JSON.parse(data); var get_files; if (module && module.indexOf("dosbox") === 0) { emulator_logo = images.dosbox; cfgr = DosBoxLoader; get_files = get_dosbox_files; } else if (module && module.indexOf("sae-") === 0) { emulator_logo = images.sae; cfgr = SAELoader; get_files = get_sae_files; } else if (module && module.indexOf("pce-") === 0) { emulator_logo = images.pce; cfgr = PCELoader; get_files = get_pce_files; } else if (module) { emulator_logo = images.mame; cfgr = MAMELoader; get_files = get_mame_files; } else { throw new Error("Unknown module type "+ module +"; cannot configure the emulator."); } var wantsWASM = modulecfg.wasm_filename && 'WebAssembly' in window; var nr = modulecfg['native_resolution']; config_args = [cfgr.emulatorJS(get_js_url(wantsWASM ? modulecfg.wasmjs_filename : modulecfg.js_filename)), cfgr.emulatorWASM(wantsWASM && get_js_url(modulecfg.wasm_filename)), cfgr.locateAdditionalEmulatorJS(locateAdditionalJS), cfgr.fileSystemKey(game), cfgr.nativeResolution(nr[0], nr[1]), cfgr.aspectRatio(nr[0] / nr[1]), cfgr.sampleRate(SAMPLE_RATE)]; if ('keepAspect' in cfgr) { cfgr.keepAspect(modulecfg.keepAspect); } if (/archive\.org$/.test(document.location.hostname)) { cfgr.muted(!(typeof $ !== 'undefined' && $.cookie && $.cookie('unmute'))); } if (module && module.indexOf("dosbox") === 0) { config_args.push(cfgr.startExe(metadata.getElementsByTagName("emulator_start") .item(0) .textContent)); } else if (module && module.indexOf("sae-") === 0) { config_args.push(cfgr.model(modulecfg.driver), cfgr.rom(modulecfg.bios_filenames)); } else if (module && module.indexOf("pce-") === 0) { config_args.push(cfgr.model(modulecfg.driver)); } else if (module) { // MAME config_args.push(cfgr.driver(modulecfg.driver), cfgr.extraArgs(modulecfg.extra_args)); } splash.setTitle("Downloading data..."); return Promise.all(get_files(cfgr, metadata, modulecfg, filelist)); }, function () { if (splash.failed_loading) { return; } splash.setTitle("Failed to download emulator metadata!"); splash.failed_loading = true; reject(2); }) .then(function (game_files) { if (splash.failed_loading) { return; } updateLogo(); resolve(cfgr.apply(null, extend(config_args, game_files))); }, function () { if (splash.failed_loading) { return; } splash.setTitle("Failed to configure emulator!"); splash.failed_loading = true; reject(3); }); }); } function locateAdditionalJS(filename) { if ("file_locations" in modulecfg && filename in modulecfg.file_locations) { return get_js_url(modulecfg.file_locations[filename]); } return get_js_url(filename); } function get_dosbox_files(cfgr, metadata, modulecfg, filelist) { var default_drive = "c", // pick any drive letter as a default drives = {}, files = [], meta = dict_from_xml(metadata); if (game && game.endsWith(".zip")) { drives[default_drive] = game; } files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) { drives[default_drive] = file.name; }); meta_props_matching(meta, /^dosbox_drive_([a-zA-Z])$/).forEach(function (result) { let key = result[0], match = result[1]; drives[match[1]] = meta[key]; }); var mounts = Object.keys(drives), len = mounts.length; mounts.forEach(function (drive, i) { var title = "Hard Disk ("+ (i+1) +" of "+ len +")", filename = drives[drive], url = (filename.includes("/")) ? get_zip_url(filename) : get_zip_url(filename, get_item_name(game)); if (filename.toLowerCase().endsWith(".zip")) { files.push(cfgr.mountZip(drive, cfgr.fetchFile(title, url))); } else { files.push(cfgr.mountFile('/'+ filename, cfgr.fetchFile(title, url))); } }); return files; } function get_mame_files(cfgr, metadata, modulecfg, filelist) { var files = [], bios_files = modulecfg['bios_filenames']; bios_files.forEach(function (fname, i) { if (fname) { var title = "Bios File ("+ (i+1) +" of "+ bios_files.length +")"; files.push(cfgr.mountFile('/'+ fname, cfgr.fetchFile(title, get_bios_url(fname)))); } }); var meta = dict_from_xml(metadata), peripherals = {}, game_files_counter = {}; files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) { game_files_counter[file.name] = 1; if (modulecfg.peripherals && modulecfg.peripherals[i]) { peripherals[modulecfg.peripherals[i]] = file.name; } }); meta_props_matching(meta, /^mame_peripheral_([a-zA-Z0-9]+)$/).forEach(function (result) { let key = result[0], match = result[1]; peripherals[match[1]] = meta[key]; game_files_counter[meta[key]] = 1; }); var game_files = Object.keys(game_files_counter), len = game_files.length; game_files.forEach(function (filename, i) { var title = "Hard Disk ("+ (i+1) +" of "+ len +")", url = (filename.includes("/")) ? get_zip_url(filename) : get_zip_url(filename, get_item_name(game)); files.push(cfgr.mountFile('/'+ filename, cfgr.fetchFile(title, url))); }); Object.keys(peripherals).forEach(function (periph) { files.push(cfgr.peripheral(periph, // we're not pushing a 'file' here, peripherals[periph])); // but that's ok }); files.push(cfgr.mountFile('/'+ modulecfg['driver'] + '.cfg', cfgr.fetchOptionalFile("CFG File", get_other_emulator_config_url(module)))); return files; } function get_sae_files(cfgr, metadata, modulecfg, filelist) { var files = [], bios_files = modulecfg['bios_filenames']; bios_files.forEach(function (fname, i) { if (fname) { var title = "Bios File ("+ (i+1) +" of "+ bios_files.length +")"; files.push(cfgr.mountFile('/'+ fname, cfgr.fetchFile(title, get_bios_url(fname)))); } }); var meta = dict_from_xml(metadata), game_files = files_with_ext_from_filelist(filelist, meta.emulator_ext); game_files.forEach(function (file, i) { if (file) { var title = "Hard Disk ("+ (i+1) +" of "+ game_files.length +")", url = (file.name.includes("/")) ? get_zip_url(file.name) : get_zip_url(file.name, get_item_name(game)); files.push(cfgr.mountFile('/'+ file.name, cfgr.fetchFile(title, url))); files.push(cfgr.floppy(0, // we're not pushing a file here file.name)); // but that's ok } }); files.push(cfgr.mountFile('/'+ modulecfg['driver'] + '.cfg', cfgr.fetchOptionalFile("Config File", get_other_emulator_config_url(module)))); return files; } function get_pce_files(cfgr, metadata, modulecfg, filelist) { var files = [], bios_files = modulecfg['bios_filenames']; bios_files.forEach(function (fname, i) { if (fname) { var title = "ROM File ("+ (i+1) +" of "+ bios_files.length +")"; files.push(cfgr.mountFile('/'+ fname, cfgr.fetchFile(title, get_bios_url(fname)))); } }); var meta = dict_from_xml(metadata), game_files_counter = {}; files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) { if (modulecfg.peripherals && modulecfg.peripherals[i]) { game_files_counter[file.name] = modulecfg.peripherals[i]; } }); meta_props_matching(meta, /^pce_drive_([a-zA-Z0-9]+)$/).forEach(function (result) { var key = result[0], periph = result[1][1]; game_files_counter[meta[key]] = periph; }); var game_files = Object.keys(game_files_counter), len = game_files.length; game_files.forEach(function (filename, i) { var title = "Hard Disk ("+ (i+1) +" of "+ len +")", ext = filename.match(/\.([^.]*)$/)[1], url = (filename.includes("/")) ? get_zip_url(filename) : get_zip_url(filename, get_item_name(game)); files.push(cfgr.mountFile('/'+ game_files_counter[filename] +'.'+ ext, cfgr.fetchFile(title, url))); }); files.push(cfgr.mountFile('/pce-'+ modulecfg['driver'] + '.cfg', cfgr.fetchOptionalFile("Config File", get_other_emulator_config_url("pce-"+ modulecfg['driver'])))); return files; } var get_item_name = function (game_path) { return game_path.split('/').shift(); }; var get_game_name = function (game_path) { return game_path.split('/').pop(); }; // NOTE: deliberately use cors.archive.org since this will 302 rewrite to iaXXXXX.us.archive.org/XX/items/... // and need to keep that "artificial" extra domain-ish name to avoid CORS issues with IE/Safari (tracey@archive) var get_emulator_config_url = function (module) { return 'emularity_engine_v1/' + module + '.json'; }; var get_other_emulator_config_url = function (module) { return 'emularity_config_v1/' + module + '.cfg'; }; var get_meta_url = function (game_path) { var path = game_path.split('/'); return "meta.xml"; }; var get_files_url = function (game_path) { var path = game_path.split('/'); return "files.xml"; }; var get_zip_url = function (game_path, item_path) { if (item_path) { return item_path +"/"+ game_path; } return game_path; }; var get_js_url = function (js_filename) { console.log(">>>>", js_filename); if (js_filename.endsWith(".gz")) { console.log("OKKK", "emularity_engine_v1/"+ js_filename.slice(0, -3)); return "emularity_engine_v1/"+ js_filename.slice(0, -3); } return "emularity_engine_v1/"+ js_filename; }; var get_bios_url = function (bios_filename) { return "emularity_bios_v1/"+ bios_filename; }; function mountat (drive) { return function (data) { return { drive: drive, mountpoint: "/" + drive, data: data }; }; } return emulator; } /** * BaseLoader */ function BaseLoader() { return Array.prototype.reduce.call(arguments, extend); } BaseLoader.canvas = function (id) { var elem = id instanceof Element ? id : document.getElementById(id); return { canvas: elem }; }; BaseLoader.emulatorJS = function (url) { return { emulatorJS: url }; }; BaseLoader.emulatorWASM = function (url) { return { emulatorWASM: url }; }; BaseLoader.locateAdditionalEmulatorJS = function (func) { return { locateAdditionalJS: func }; }; BaseLoader.fileSystemKey = function (key) { return { fileSystemKey: key }; }; BaseLoader.nativeResolution = function (width, height) { if (typeof width !== 'number' || typeof height !== 'number') throw new Error("Width and height must be numbers"); return { nativeResolution: { width: Math.floor(width), height: Math.floor(height) } }; }; BaseLoader.aspectRatio = function (ratio) { if (typeof ratio !== 'number') throw new Error("Aspect ratio must be a number"); return { aspectRatio: ratio }; }; BaseLoader.sampleRate = function (rate) { return { sample_rate: rate }; }; BaseLoader.muted = function (muted) { return { muted: muted }; }; BaseLoader.mountZip = function (drive, file) { return { files: [{ drive: drive, mountpoint: "/" + drive, file: file }] }; }; BaseLoader.mountFile = function (filename, file) { return { files: [{ mountpoint: filename, file: file }] }; }; BaseLoader.fetchFile = function (title, url) { return { title: title, url: url, optional: false }; }; BaseLoader.fetchOptionalFile = function (title, url) { return { title: title, url: url, optional: true }; }; BaseLoader.localFile = function (title, data) { return { title: title, data: data }; }; /** * DosBoxLoader */ function DosBoxLoader() { var config = Array.prototype.reduce.call(arguments, extend); config.emulator_arguments = build_dosbox_arguments(config.emulatorStart, config.files, config.extra_dosbox_args); config.runner = EmscriptenRunner; return config; } DosBoxLoader.__proto__ = BaseLoader; DosBoxLoader.startExe = function (path) { return { emulatorStart: path }; }; DosBoxLoader.extraArgs = function (args) { return { extra_dosbox_args: args }; }; /** * MAMELoader */ function MAMELoader() { var config = Array.prototype.reduce.call(arguments, extend); config.emulator_arguments = build_mame_arguments(config.muted, config.mame_driver, config.nativeResolution, config.sample_rate, config.peripheral, config.extra_mame_args, config.keep_aspect); config.runner = MAMERunner; return config; } MAMELoader.__proto__ = BaseLoader; MAMELoader.driver = function (driver) { return { mame_driver: driver }; }; MAMELoader.peripheral = function (peripheral, game) { var p = {}; p[peripheral] = [game]; return { peripheral: p }; }; MAMELoader.keepAspect = function (keep) { return { keep_aspect: !!keep }; }; MAMELoader.extraArgs = function (args) { return { extra_mame_args: args }; }; /** * SAELoader */ function SAELoader() { var config = Array.prototype.reduce.call(arguments, extend); config.runner = SAERunner; return config; } SAELoader.__proto__ = BaseLoader; SAELoader.model = function (model) { return { amigaModel: model }; }; SAELoader.fastMemory = function (megabytes) { return { fast_memory: megabytes << 20 }; }; SAELoader.rom = function (filenames) { if (typeof filenames == "string") filenames = [filenames]; return { rom: filenames[0], extRom: filenames[1] }; }; SAELoader.floppy = function (index, filename) { var f = {}; f[index] = filename; return { floppy: f }; }; SAELoader.ntsc = function (v) { return { ntsc: !!v }; }; /** * PCELoader */ function PCELoader() { var config = Array.prototype.reduce.call(arguments, extend); config.emulator_arguments = ["-c", "/emulator/pce-"+ config.pceModel +".cfg"]; config.runner = EmscriptenRunner; return config; } PCELoader.__proto__ = BaseLoader; PCELoader.model = function (model) { return { pceModel: model }; }; var build_mame_arguments = function (muted, driver, native_resolution, sample_rate, peripheral, extra_args, keepaspect) { var args = [driver, '-verbose', '-rompath', 'emulator', '-window', keepaspect ? '-keepaspect' : '-nokeepaspect']; if (native_resolution && "width" in native_resolution && "height" in native_resolution) { args.push('-resolution', [native_resolution.width, native_resolution.height].join('x')); } if (muted) { args.push('-sound', 'none'); } else if (sample_rate) { args.push('-samplerate', sample_rate); } if (peripheral) { for (var p in peripheral) { if (Object.prototype.propertyIsEnumerable.call(peripheral, p)) { args.push('-' + p, '/emulator/'+ (peripheral[p][0].replace(/\//g,'_'))); } } } if (extra_args) { args = args.concat(extra_args); } return args; }; var build_dosbox_arguments = function (emulator_start, files, extra_args) { var args = ['-conf', '/emulator/dosbox.conf']; var len = files.length; for (var i = 0; i < len; i++) { if ('drive' in files[i]) { args.push('-c', 'mount '+ files[i].drive +' /emulator'+ files[i].mountpoint); } } if (extra_args) { args = args.concat(extra_args); } var path = emulator_start.split(/\\|\//); // I have LTS already args.push('-c', /^[a-zA-Z]:$/.test(path[0]) ? path.shift() : 'c:'); var prog = path.pop(); if (path && path.length) args.push('-c', 'cd '+ path.join('/')); args.push('-c', prog); return args; }; /* * EmscriptenRunner */ function EmscriptenRunner(canvas, game_data) { var self = this; this._hooks = { start: [], reset: [] }; // This is somewhat wrong, because our Emscripten-based emulators // are currently compiled to start immediately when their js file // is loaded. Module = { arguments: game_data.emulator_arguments, screenIsReadOnly: true, print: function (text) { console.log(text); }, printErr: function (text) { console.log(text); }, canvas: canvas, noInitialRun: false, locateFile: game_data.locateAdditionalJS, wasmBinary: game_data.wasmBinary, preInit: function () { // Re-initialize BFS to just use the writable in-memory storage. BrowserFS.initialize(game_data.fs); var BFS = new BrowserFS.EmscriptenFS(); // Mount the file system into Emscripten. FS.mkdir('/emulator'); FS.mount(BFS, {root: '/'}, '/emulator'); }, preRun: [function () { self._hooks.start.forEach(function (f) { //try { f && f(); //} catch(x) { // console.warn(x); //} }); }] }; } EmscriptenRunner.prototype.start = function () { }; EmscriptenRunner.prototype.pause = function () { }; EmscriptenRunner.prototype.stop = function () { }; EmscriptenRunner.prototype.mute = function () { try { if (!SDL_PauseAudio) SDL_PauseAudio = Module.cwrap('SDL_PauseAudio', '', ['number']); SDL_PauseAudio(true); } catch (x) { console.log("Unable to change audio state:", x); } }; EmscriptenRunner.prototype.unmute = function () { try { if (!SDL_PauseAudio) SDL_PauseAudio = Module.cwrap('SDL_PauseAudio', '', ['number']); SDL_PauseAudio(false); } catch (x) { console.log("Unable to change audio state:", x); } }; EmscriptenRunner.prototype.onStarted = function (func) { this._hooks.start.push(func); }; EmscriptenRunner.prototype.onReset = function (func) { this._hooks.reset.push(func); }; EmscriptenRunner.prototype.requestFullScreen = function () { }; /* * MAMERunner */ function MAMERunner() { return EmscriptenRunner.apply(this, arguments); } MAMERunner.prototype = Object.create(EmscriptenRunner.prototype, { mute: function () { var soundmgr = Module.__ZN15running_machine20emscripten_get_soundEv(Module.__ZN15running_machine30emscripten_get_running_machineEv()); Module.__ZN13sound_manager4muteEbh(soundmgr, true, 0x02); // MUTE_REASON_UI }, unmute: function () { var soundmgr = Module.__ZN15running_machine20emscripten_get_soundEv(Module.__ZN15running_machine30emscripten_get_running_machineEv()); Module.__ZN13sound_manager4muteEbh(soundmgr, false, 0x02); // MUTE_REASON_UI } }); /* * SAERunner */ function SAERunner(canvas, game_data) { this._sae = new ScriptedAmigaEmulator(); this._cfg = this._sae.getConfig(); this._canvas = canvas; var model = null; switch (game_data.amigaModel) { case "A500": model = SAEC_Model_A500; break; case "A500P": model = SAEC_Model_A500P; break; case "A600": model = SAEC_Model_A600; break; case "A1000": model = SAEC_Model_A1000; break; case "A1200": model = SAEC_Model_A1200; break; case "A2000": model = SAEC_Model_A2000; break; case "A3000": model = SAEC_Model_A3000; break; case "A4000": model = SAEC_Model_A4000; break; case "A4000T": model = SAEC_Model_A4000T; break; /* future. do not use. cd-emulation is not implemented yet. case "CDTV": model = SAEC_Model_CDTV; break; case "CD32": model = SAEC_Model_CD32; break; */ } this._sae.setModel(model, 0); this._cfg.memory.z2FastSize = game_data.fastMemory || 2 << 20; this._cfg.floppy.speed = SAEC_Config_Floppy_Speed_Turbo; this._cfg.video.id = canvas.getAttribute("id"); if (game_data.nativeResolution && game_data.nativeResolution.height == 360 && game_data.nativeResolution.width == 284) { this._cfg.video.hresolution = SAEC_Config_Video_HResolution_LoRes; this._cfg.video.vresolution = SAEC_Config_Video_VResolution_NonDouble; this._cfg.video.size_win.width = SAEC_Video_DEF_AMIGA_WIDTH; /* 360 */ this._cfg.video.size_win.height = SAEC_Video_DEF_AMIGA_HEIGHT; /* 284 */ } else if (game_data.nativeResolution && game_data.nativeResolution.height == 1440 && game_data.nativeResolution.width == 568) { this._cfg.video.hresolution = SAEC_Config_Video_HResolution_SuperHiRes; this._cfg.video.vresolution = SAEC_Config_Video_VResolution_Double; this._cfg.video.size_win.width = SAEC_Video_DEF_AMIGA_WIDTH << 2; /* 1440 */ this._cfg.video.size_win.height = SAEC_Video_DEF_AMIGA_HEIGHT << 1; /* 568 */ } else { this._cfg.video.hresolution = SAEC_Config_Video_HResolution_HiRes; this._cfg.video.vresolution = SAEC_Config_Video_VResolution_Double; this._cfg.video.size_win.width = SAEC_Video_DEF_AMIGA_WIDTH << 1; /* 720 */ this._cfg.video.size_win.height = SAEC_Video_DEF_AMIGA_HEIGHT << 1; /* 568 */ } this._cfg.memory.rom.name = game_data.rom; this._cfg.memory.rom.data = game_data.fs.readFileSync('/'+game_data.rom, null, flag_r); this._cfg.memory.rom.size = this._cfg.memory.rom.data.length; if (game_data.extRom) { this._cfg.memory.extRom.name = game_data.extRom; this._cfg.memory.extRom.data = game_data.fs.readFileSync('/'+game_data.extRom, null, flag_r); this._cfg.memory.extRom.size = this._cfg.memory.extRom.data.length; } this._cfg.floppy.drive[0].file.name = game_data.floppy[0]; this._cfg.floppy.drive[0].file.data = game_data.fs.readFileSync('/'+game_data.floppy[0], null, flag_r); this._cfg.floppy.drive[0].file.size = this._cfg.floppy.drive[0].file.data.length; } SAERunner.prototype.start = function () { var err = this._sae.start(); }; SAERunner.prototype.pause = function () { this._sae.pause(); }; SAERunner.prototype.stop = function () { this._sae.stop(); }; SAERunner.prototype.mute = function () { var err = this._sae.mute(true); if (err) { console.warn("unable to mute; SAE error number", err); } }; SAERunner.prototype.unmute = function () { var err = this._sae.mute(false); if (err) { console.warn("unable to unmute; SAE error number", err); } }; SAERunner.prototype.onStarted = function (func) { this._cfg.hook.event.started = func; }; SAERunner.prototype.onReset = function (func) { this._cfg.hook.event.reseted = func; }; SAERunner.prototype.requestFullScreen = function () { getfullscreenenabler().call(this._canvas); }; /** * Emulator */ function Emulator(canvas, callbacks, loadFiles) { if (typeof callbacks !== 'object') { callbacks = { before_emulator: null, before_run: callbacks }; } var js_url; var requests = []; var drawloadingtimer; // TODO: Have an enum value that communicates the current state of the emulator, e.g. 'initializing', 'loading', 'running'. var has_started = false; var loading = false; var defaultSplashColors = { foreground: 'white', background: 'black', failure: 'red' }; var splash = { loading_text: "", spinning: true, finished_loading: false, colors: defaultSplashColors, table: null, splashimg: new Image() }; var runner; var muted = false; var SDL_PauseAudio; this.isMuted = function () { return muted; }; this.mute = function () { return this.setMute(true); }; this.unmute = function () { return this.setMute(false); }; this.toggleMute = function () { return this.setMute(!muted); }; this.setMute = function (state) { muted = state; if (runner) { if (state) { runner.mute(); } else { runner.unmute(); } } else { try { if (!SDL_PauseAudio) SDL_PauseAudio = Module.cwrap('SDL_PauseAudio', '', ['number']); SDL_PauseAudio(state); } catch (x) { console.log("Unable to change audio state:", x); } } return this; }; // This is the bare minimum that will allow gamepads to work. If // we don't listen for them then the browser won't tell us about // them. // TODO: add hooks so that some kind of UI can be displayed. window.addEventListener("gamepadconnected", function (e) { console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.", e.gamepad.index, e.gamepad.id, e.gamepad.buttons.length, e.gamepad.axes.length); }); window.addEventListener("gamepaddisconnected", function (e) { console.log("Gamepad disconnected from index %d: %s", e.gamepad.index, e.gamepad.id); }); if (/archive\.org$/.test(document.location.hostname) && document.getElementById("gofullscreen")) { document.getElementById("gofullscreen").addEventListener("click", this.requestFullScreen); } var css_resolution, scale, aspectRatio; // right off the bat we set the canvas's inner dimensions to // whatever it's current css dimensions are; this isn't likely to be // the same size that dosbox/jsmame will set it to, but it avoids // the case where the size was left at the default 300x150 if (!canvas.hasAttribute("width")) { var style = getComputedStyle(canvas); canvas.width = parseInt(style.width, 10); canvas.height = parseInt(style.height, 10); } this.setScale = function(_scale) { scale = _scale; return this; }; this.setSplashImage = function(_splashimg) { if (_splashimg) { if (_splashimg instanceof Image) { if (splash.splashimg.parentNode) { splash.splashimg.src = _splashimg.src; } else { splash.splashimg = _splashimg; } } else { splash.splashimg.src = _splashimg; } } return this; }; this.setCSSResolution = function(_resolution) { css_resolution = _resolution; return this; }; this.setAspectRatio = function(_aspectRatio) { aspectRatio = _aspectRatio; return this; }; this.setCallbacks = function(_callbacks) { if (typeof _callbacks !== 'object') { callbacks = { before_emulator: null, before_run: _callbacks }; } else { callbacks = _callbacks; } return this; }; this.setSplashColors = function (colors) { splash.colors = colors; return this; }; this.setLoad = function (loadFunc) { loadFiles = loadFunc; return this; }; var start = function (options) { if (has_started) return false; has_started = true; var defaultOptions = { waitAfterDownloading: false, hasCustomCSS: false }; if (typeof options !== 'object') { options = defaultOptions; } else { options.__proto__ = defaultOptions; } var k, c, game_data; setupSplash(canvas, splash, options); drawsplash(); var loading; if (typeof loadFiles === 'function') { loading = loadFiles(fetch_file, splash); } else { loading = Promise.resolve(loadFiles); } loading.then(function (_game_data) { return new Promise(function(resolve, reject) { var inMemoryFS = new BrowserFS.FileSystem.InMemory(); // If the browser supports IndexedDB storage, mirror writes to that storage // for persistence purposes. if (BrowserFS.FileSystem.IndexedDB.isAvailable()) { var AsyncMirrorFS = BrowserFS.FileSystem.AsyncMirror, IndexedDB = BrowserFS.FileSystem.IndexedDB; deltaFS = new AsyncMirrorFS(inMemoryFS, new IndexedDB(function(e, fs) { if (e) { // we probably weren't given access; // private window for example. // don't fail completely, just don't // use indexeddb deltaFS = inMemoryFS; finish(); } else { // Initialize deltaFS by copying files from async storage to sync storage. deltaFS.initialize(function (e) { if (e) { reject(e); } else { finish(); } }); } }, "fileSystemKey" in _game_data ? _game_data.fileSystemKey : "emularity")); } else { finish(); } function finish() { game_data = _game_data; // Any file system writes to MountableFileSystem will be written to the // deltaFS, letting us mount read-only zip files into the MountableFileSystem // while being able to "write" to them. game_data.fs = new BrowserFS.FileSystem.OverlayFS(deltaFS, new BrowserFS.FileSystem.MountableFileSystem()); game_data.fs.initialize(function (e) { if (e) { console.error("Failed to initialize the OverlayFS:", e); reject(); } else { var Buffer = BrowserFS.BFSRequire('buffer').Buffer; function fetch(file) { if ('data' in file && file.data !== null && typeof file.data !== 'undefined') { return Promise.resolve(file.data); } return fetch_file(file.title, file.url, 'arraybuffer', file.optional); } function mountat(drive) { return function (data) { if (data !== null) { drive = drive.toLowerCase(); var mountpoint = '/'+ drive; // Mount into RO MFS. game_data.fs.getOverlayedFileSystems().readable.mount(mountpoint, BFSOpenZip(new Buffer(data))); } }; } function saveat(filename) { return function (data) { if (data !== null) { if (filename.includes('/')) { var parts = filename.split('/'); for (var i = 1; i < parts.length; i++) { var path = '/'+ parts.slice(0, i).join('/'); if (!game_data.fs.existsSync(path)) { game_data.fs.mkdirSync(path); } } } game_data.fs.writeFileSync('/'+ filename, new Buffer(data), null, flag_w, 0x1a4); } }; } var promises = game_data.files .map(function (f) { if (f && f.file) { if (f.drive) { return fetch(f.file).then(mountat(f.drive)); } else if (f.mountpoint) { return fetch(f.file).then(saveat(f.mountpoint)); } } return null; }); // this is kinda wrong; it really only applies when we're loading something created by Emscripten if ('emulatorWASM' in game_data && game_data.emulatorWASM && 'WebAssembly' in window) { promises.push(fetch({ title: "WASM Binary", url: game_data.emulatorWASM }).then(function (data) { game_data.wasmBinary = data; })); } Promise.all(promises).then(resolve, reject); } }); } }); }) .then(function (game_files) { if (!game_data || splash.failed_loading) { return null; } if (options.waitAfterDownloading) { return new Promise(function (resolve, reject) { splash.setTitle("Press any key to continue..."); splash.spinning = false; // stashes these event listeners so that we can remove them after window.addEventListener('keypress', k = keyevent(resolve)); canvas.addEventListener('click', c = resolve); splash.splashElt.addEventListener('click', c); }); } return Promise.resolve(); }, function () { if (splash.failed_loading) { return; } splash.setTitle("Failed to download data!"); splash.failed_loading = true; }) .then(function () { if (!game_data || splash.failed_loading) { return null; } splash.spinning = true; window.removeEventListener('keypress', k); canvas.removeEventListener('click', c); splash.splashElt.removeEventListener('click', c); // Don't let arrow, pg up/down, home, end affect page position blockSomeKeys(); setupFullScreen(); disableRightClickContextMenu(canvas); // Emscripten doesn't use the proper prefixed functions for fullscreen requests, // so let's map the prefixed versions to the correct function. canvas.requestPointerLock = getpointerlockenabler(); moveConfigToRoot(game_data.fs); if (callbacks && callbacks.before_emulator) { try { callbacks.before_emulator(); } catch (x) { console.log(x); } } if ("runner" in game_data) { if (game_data.runner == EmscriptenRunner || game_data.runner == MAMERunner) { // this is a stupid hack. Emscripten-based // apps currently need the runner to be set // up first, then we can attach the // script. The others have to do it the // other way around. runner = setup_runner(); } } if (game_data.emulatorJS) { splash.setTitle("Launching System 7"); return attach_script(game_data.emulatorJS); } else { splash.setTitle("Non-system disk or disk error"); } return null; }, function () { if (!game_data || splash.failed_loading) { return null; } splash.setTitle("Invalid media, track 0 bad or unusable"); splash.failed_loading = true; }) .then(function () { if (!game_data || splash.failed_loading) { return null; } if ("runner" in game_data) { if (!runner) { runner = setup_runner(); } runner.start(); } }); function setup_runner() { var runner = new game_data.runner(canvas, game_data); resizeCanvas(canvas, 1, game_data.nativeResolution, game_data.aspectRatio); runner.onStarted(function () { splash.finished_loading = true; splash.hide(); if (callbacks && callbacks.before_run) { setTimeout(function() { callbacks.before_run(); }, 0); } }); runner.onReset(function () { if (muted) { runner.mute(); } }); return runner; } return this; }; this.start = start; var formatSize = function (event) { if (event.lengthComputable) return "("+ (event.total ? (event.loaded / event.total * 100).toFixed(0) : "100") + "%; "+ formatBytes(event.loaded) + " of "+ formatBytes(event.total) +")"; return "("+ formatBytes(event.loaded) +")"; }; var formatBytes = function (bytes, base10) { if (bytes === 0) return "0 B"; var unit = base10 ? 1000 : 1024, units = base10 ? ["B", "kB","MB","GB","TB","PB","EB","ZB","YB"] : ["B", "KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"], exp = parseInt((Math.log(bytes) / Math.log(unit))), size = bytes / Math.pow(unit, exp); return size.toFixed(1) +' '+ units[exp]; }; var fetch_file = function (title, url, rt, optional) { var needsCSS = splash.table.dataset.hasCustomCSS == "false"; var row = addRow(splash.table); var titleCell = row[0], statusCell = row[1]; titleCell.textContent = title; return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = rt || 'arraybuffer'; xhr.onprogress = function (e) { titleCell.innerHTML = title +" "+ formatSize(e) +""; }; xhr.onload = function (e) { if (xhr.status === 200) { success(); resolve(xhr.response); } else if (optional) { success(); resolve(null); } else { failure(); reject(); } }; xhr.onerror = function (e) { if (optional) { success(); resolve(null); } else { failure(); reject(); } }; function success() { statusCell.textContent = "✔"; titleCell.parentNode.classList.add('emularity-download-success'); titleCell.textContent = title; if (needsCSS) { titleCell.style.fontWeight = 'bold'; titleCell.parentNode.style.backgroundColor = splash.getColor('foreground'); titleCell.parentNode.style.color = splash.getColor('background'); } } function failure() { statusCell.textContent = "✘"; titleCell.parentNode.classList.add('emularity-download-failure'); titleCell.textContent = title; if (needsCSS) { titleCell.style.fontWeight = 'bold'; titleCell.parentNode.style.backgroundColor = splash.getColor('failure'); titleCell.parentNode.style.color = splash.getColor('background'); } } xhr.send(); }); }; function keyevent(resolve) { return function (e) { if (e.which == 32) { e.preventDefault(); resolve(); } }; }; var resizeCanvas = function (canvas, scale, resolution, aspectRatio) { if (scale && resolution) { // optimizeSpeed is the standardized value. different // browsers support different values; they will all ignore // values that they don't understand. canvas.style.imageRendering = '-moz-crisp-edges'; canvas.style.imageRendering = '-o-crisp-edges'; canvas.style.imageRendering = '-webkit-optimize-contrast'; canvas.style.imageRendering = 'optimize-contrast'; canvas.style.imageRendering = 'crisp-edges'; canvas.style.imageRendering = 'pixelated'; canvas.style.imageRendering = 'optimizeSpeed'; canvas.style.width = resolution.width * scale +'px'; canvas.style.height = resolution.height * scale +'px'; canvas.width = resolution.width; canvas.height = resolution.height; } }; var clearCanvas = function () { var context = canvas.getContext('2d'); context.fillStyle = splash.getColor('background'); context.fillRect(0, 0, canvas.width, canvas.height); console.log("canvas cleared"); }; function setupSplash(canvas, splash, globalOptions) { splash.splashElt = document.getElementById("emularity-splash-screen"); if (!splash.splashElt) { splash.splashElt = document.createElement('div'); splash.splashElt.classList.add("emularity-splash-screen"); if (!globalOptions.hasCustomCSS) { splash.splashElt.style.position = 'absolute'; splash.splashElt.style.top = '0'; splash.splashElt.style.left = '0'; splash.splashElt.style.right = '0'; splash.splashElt.style.color = splash.getColor('foreground'); splash.splashElt.style.backgroundColor = splash.getColor('background'); } canvas.parentElement.appendChild(splash.splashElt); } splash.splashimg.classList.add("emularity-splash-image"); if (!globalOptions.hasCustomCSS) { splash.splashimg.style.display = 'block'; splash.splashimg.style.marginLeft = 'auto'; splash.splashimg.style.marginRight = 'auto'; } splash.splashElt.appendChild(splash.splashimg); splash.titleElt = document.createElement('span'); splash.titleElt.classList.add("emularity-splash-title"); if (!globalOptions.hasCustomCSS) { splash.titleElt.style.display = 'block'; splash.titleElt.style.width = '100%'; splash.titleElt.style.marginTop = "1em"; splash.titleElt.style.marginBottom = "1em"; splash.titleElt.style.textAlign = 'center'; splash.titleElt.style.font = "24px sans-serif"; } splash.titleElt.textContent = " "; splash.splashElt.appendChild(splash.titleElt); var table = document.getElementById("emularity-progress-indicator"); if (!table) { table = document.createElement('table'); table.classList.add("emularity-progress-indicator"); table.dataset.hasCustomCSS = globalOptions.hasCustomCSS; if (!globalOptions.hasCustomCSS) { table.style.width = "75%"; table.style.color = splash.getColor('foreground'); table.style.backgroundColor = splash.getColor('background'); table.style.marginLeft = 'auto'; table.style.marginRight = 'auto'; table.style.borderCollapse = 'separate'; table.style.borderSpacing = "2px"; } splash.splashElt.appendChild(table); } splash.table = table; } splash.setTitle = function (title) { splash.titleElt.textContent = title; }; splash.hide = function () { splash.splashElt.style.display = 'none'; }; splash.getColor = function (name) { return name in splash.colors ? splash.colors[name] : defaultSplashColors[name]; }; var addRow = function (table) { var needsCSS = table.dataset.hasCustomCSS == "false"; var row = table.insertRow(-1); if (needsCSS) { row.style.textAlign = 'center'; } var cell = row.insertCell(-1); if (needsCSS) { cell.style.position = 'relative'; } var titleCell = document.createElement('span'); titleCell.classList.add("emularity-download-title"); titleCell.textContent = '—'; if (needsCSS) { titleCell.style.verticalAlign = 'center'; titleCell.style.minHeight = "24px"; titleCell.style.whiteSpace = "nowrap"; } cell.appendChild(titleCell); var statusCell = document.createElement('span'); statusCell.classList.add("emularity-download-status"); if (needsCSS) { statusCell.style.position = 'absolute'; statusCell.style.left = "0"; statusCell.style.paddingLeft = "0.5em"; } cell.appendChild(statusCell); return [titleCell, statusCell]; }; var drawsplash = function () { canvas.setAttribute('moz-opaque', ''); if (!splash.splashimg.src) { splash.splashimg.src = "logo/emularity_color_small.png"; } }; function attach_script(js_url) { return new Promise(function (resolve, reject) { var newScript; function loaded(e) { if (e.target == newScript) { newScript.removeEventListener("load", loaded); newScript.removeEventListener("error", failed); resolve(); } } function failed(e) { if (e.target == newScript) { newScript.removeEventListener("load", loaded); newScript.removeEventListener("error", failed); reject(); } } if (js_url) { var head = document.getElementsByTagName('head')[0]; newScript = document.createElement('script'); newScript.addEventListener("load", loaded); newScript.addEventListener("error", failed); newScript.type = 'text/javascript'; newScript.src = js_url; head.appendChild(newScript); } }); } function getpointerlockenabler() { return canvas.requestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock; } this.isfullscreensupported = function () { return !!(getfullscreenenabler()); }; function setupFullScreen() { var self = this; var fullScreenChangeHandler = function() { if (!(document.mozFullScreenElement || document.fullScreenElement)) { resizeCanvas(canvas, scale, css_resolution, aspectRatio); } }; if ('onfullscreenchange' in document) { document.addEventListener('fullscreenchange', fullScreenChangeHandler); } else if ('onmozfullscreenchange' in document) { document.addEventListener('mozfullscreenchange', fullScreenChangeHandler); } else if ('onwebkitfullscreenchange' in document) { document.addEventListener('webkitfullscreenchange', fullScreenChangeHandler); } }; this.requestFullScreen = function () { if (typeof Module == "object" && "requestFullScreen" in Module) { Module.requestFullScreen(1, 0); } else if (runner) { runner.requestFullScreen(); } }; /** * Prevents page navigation keys such as page up/page down from * moving the page while the user is playing. */ function blockSomeKeys() { function keypress (e) { if (e.which >= 33 && e.which <= 40) { e.preventDefault(); return false; } return true; } window.onkeydown = keypress; } /** * Disables the right click menu for the given element. */ function disableRightClickContextMenu(element) { element.addEventListener('contextmenu', function (e) { if (e.button == 2) { // Block right-click menu thru preventing default action. e.preventDefault(); } }); } }; /** * misc */ function getfullscreenenabler() { return canvas.requestFullScreen || canvas.webkitRequestFullScreen || canvas.mozRequestFullScreen; } function BFSOpenZip(loadedData) { return new BrowserFS.FileSystem.ZipFS(loadedData); }; // This is such a hack. We're not calling the BrowserFS api // "correctly", so we have to synthesize these flags ourselves var flag_r = { isReadable: function() { return true; }, isWriteable: function() { return false; }, isTruncating: function() { return false; }, isAppendable: function() { return false; }, isSynchronous: function() { return false; }, isExclusive: function() { return false; }, pathExistsAction: function() { return 0; }, pathNotExistsAction: function() { return 1; } }; var flag_w = { isReadable: function() { return false; }, isWriteable: function() { return true; }, isTruncating: function() { return false; }, isAppendable: function() { return false; }, isSynchronous: function() { return false; }, isExclusive: function() { return false; }, pathExistsAction: function() { return 0; }, pathNotExistsAction: function() { return 3; } }; /** * Searches for dosbox.conf, and moves it to '/dosbox.conf' so dosbox uses it. */ function moveConfigToRoot(fs) { var dosboxConfPath = null; // Recursively search for dosbox.conf. function searchDirectory(dirPath) { fs.readdirSync(dirPath).forEach(function(item) { if (dosboxConfPath) { return; } // Avoid infinite recursion by ignoring these entries, which exist at // the root. if (item === '.' || item === '..') { return; } // Append '/' between dirPath and the item's name... unless dirPath // already ends in it (which always occurs if dirPath is the root, '/'). var itemPath = dirPath + (dirPath[dirPath.length - 1] !== '/' ? "/" : "") + item, itemStat = fs.statSync(itemPath); if (itemStat.isDirectory(itemStat.mode)) { searchDirectory(itemPath); } else if (item === 'dosbox.conf') { dosboxConfPath = itemPath; } }); } searchDirectory('/'); if (dosboxConfPath !== null) { fs.writeFileSync('/dosbox.conf', fs.readFileSync(dosboxConfPath, null, flag_r), null, flag_w, 0x1a4); } }; function extend(a, b) { if (a === null) return b; if (b === null) return a; var ta = typeof a, tb = typeof b; if (ta !== tb) { if (ta === 'undefined') return b; if (tb === 'undefined') return a; throw new Error("Cannot extend an "+ ta +" with an "+ tb); } if (Array.isArray(a)) return a.concat(b); if (ta === 'object') { Object.keys(b).forEach(function (k) { a[k] = extend(k in a ? a[k] : undefined, b[k]); }); return a; } return b; } function dict_from_xml(xml) { if (xml instanceof XMLDocument) { xml = xml.documentElement; } var dict = {}; var len = xml.childNodes.length, i; for (i = 0; i < len; i++) { var node = xml.childNodes[i]; dict[node.nodeName] = node.textContent; } return dict; } function list_from_xml(xml) { if (xml instanceof XMLDocument) { xml = xml.documentElement; } return Array.prototype.slice.call(xml.childNodes); } function files_from_filelist(xml) { return list_from_xml(xml).filter(function (node) { return "getAttribute" in node; }) .map(function (node) { var file = dict_from_xml(node); file.name = node.getAttribute("name"); return file; }); } function files_with_ext_from_filelist(xml, ext) { if (!ext) { return []; } if (!ext.startsWith('.')) { ext = '.'+ ext; } ext = ext.toLowerCase(); return files_from_filelist(xml).filter(function (file) { return file.name.toLowerCase().endsWith(ext); }); } function meta_props_matching(meta, regex) { if (typeof regex == "string") regex = RegExp(regex); return Object.keys(meta).map(function (k) { let match = regex.exec(k); if (match) return [k, match]; return null; }) .filter(function (result) { return !!result; }); } function _SDL_CreateRGBSurfaceFrom(pixels, width, height, depth, pitch, rmask, gmask, bmask, amask) { // TODO: Actually fill pixel data to created surface. // TODO: Take into account depth and pitch parameters. // console.log('TODO: Partially unimplemented SDL_CreateRGBSurfaceFrom called!'); var surface = SDL.makeSurface(width, height, 0, false, 'CreateRGBSurfaceFrom', rmask, gmask, bmask, amask); var surfaceData = SDL.surfaces[surface]; var surfaceImageData = surfaceData.ctx.getImageData(0, 0, width, height); var surfacePixelData = surfaceImageData.data; // Fill pixel data to created surface. // Supports SDL_PIXELFORMAT_RGBA8888 and SDL_PIXELFORMAT_RGB888 var channels = amask ? 4 : 3; // RGBA8888 or RGB888 for (var pixelOffset = 0; pixelOffset < width*height; pixelOffset++) { surfacePixelData[pixelOffset*4+0] = HEAPU8[pixels + (pixelOffset*channels+0)]; // R surfacePixelData[pixelOffset*4+1] = HEAPU8[pixels + (pixelOffset*channels+1)]; // G surfacePixelData[pixelOffset*4+2] = HEAPU8[pixels + (pixelOffset*channels+2)]; // B surfacePixelData[pixelOffset*4+3] = amask ? HEAPU8[pixels + (pixelOffset*channels+3)] : 0xff; // A }; surfaceData.ctx.putImageData(surfaceImageData, 0, 0); return surface; } window.IALoader = IALoader; window.DosBoxLoader = DosBoxLoader; window.JSMESSLoader = MAMELoader; // depreciated; just for backwards compatibility window.JSMAMELoader = MAMELoader; // ditto window.MAMELoader = MAMELoader; window.SAELoader = SAELoader; window.PCELoader = PCELoader; window.Emulator = Emulator; window._SDL_CreateRGBSurfaceFrom = _SDL_CreateRGBSurfaceFrom; })(typeof Promise === 'undefined' ? ES6Promise.Promise : Promise); // legacy var JSMESS = JSMESS || {}; JSMESS.ready = function (f) { f(); };