var Module = null; (function (Promise) { 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 if (/archive\.org$/.test(document.location.hostname)) { var images = { ia: img("/images/ialogo.png"), mame: img("/images/mame.png"), mess: img("/images/mess.png"), dosbox: img("/images/dosbox.png") }; } else { images = { ia: img("other_logos/ia-logo-150x150.png"), mame: img("other_logos/mame.png"), mess: img("other_logos/mess.png"), dosbox: img("other_logos/dosbox.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, module, modulecfg, config_args, emulator_logo, emulator = new Emulator(canvas).setScale(scale) .setSplashImage(images.ia) .setLoad(loadFiles) .setCallbacks(callbacks); var cfgr; function loadFiles(fetch_file, splash) { splash.setTitle("Downloading game metadata..."); return new Promise(function (resolve, reject) { var loading = fetch_file('Game Metadata', get_meta_url(game), 'document'); loading.then(function (data) { metadata = 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 () { splash.setTitle("Failed to download metadata!"); splash.failed_loading = true; reject(1); }) .then(function (data) { modulecfg = JSON.parse(data); var mame = 'arcade' in modulecfg && parseInt(modulecfg['arcade'], 10); var get_files; if (module && module.indexOf("dosbox") === 0) { emulator_logo = images.dosbox; cfgr = DosBoxLoader; get_files = get_dosbox_files; } else if (module) { if (mame) { emulator_logo = images.mame; cfgr = JSMAMELoader; get_files = get_mame_files; } else { emulator_logo = images.mess; cfgr = JSMESSLoader; get_files = get_mess_files; } } else { throw new Error("Unknown module type "+ module +"; cannot configure the emulator."); } var nr = modulecfg['native_resolution']; config_args = [cfgr.emulatorJS(get_js_url(modulecfg.js_filename)), cfgr.locateAdditionalEmulatorJS(locateAdditionalJS), cfgr.nativeResolution(nr[0], nr[1]), cfgr.aspectRatio(nr[0] / nr[1]), cfgr.sampleRate(SAMPLE_RATE), 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) { config_args.push(cfgr.driver(modulecfg.driver), cfgr.extraArgs(modulecfg.extra_args)); if (modulecfg.peripherals && modulecfg.peripherals[0]) { config_args.push(cfgr.peripheral(modulecfg.peripherals[0], get_game_name(game))); } } splash.setTitle("Downloading game data..."); return Promise.all(get_files(cfgr, metadata, modulecfg)); }, function () { splash.setTitle("Failed to download metadata!"); splash.failed_loading = true; reject(2); }) .then(function (game_files) { resolve(cfgr.apply(null, extend(config_args, game_files))); }, function () { splash.setTitle("Failed to download game data!"); 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]); } throw new Error("Don't know how to find file: "+ filename); } function get_dosbox_files(cfgr, emulator, modulecfg) { // first get the urls var urls = [], files = []; var len = metadata.documentElement.childNodes.length, i; for (i = 0; i < len; i++) { var node = metadata.documentElement.childNodes[i]; var m = node.nodeName.match(/^dosbox_drive_[a-zA-Z]$/); if (m) { urls.push(node); } } // and a count, then fetch them in var len = urls.length; for (i = 0; i < len; i++) { var node = urls[i], drive = node.nodeName.split('_')[2], title = 'Game File ('+ (i+1) +' of '+ (game ? len+1 : len) +')', url = get_zip_url(node.textContent); files.push(cfgr.mountZip(drive, cfgr.fetchFile(title, url))); } if (game) { var drive = 'c', title = 'Game File ('+ (i+1) +' of '+ (game ? len+1 : len) +')', url = get_zip_url(game); files.push(cfgr.mountZip(drive, cfgr.fetchFile(title, url))); } return files; } function get_mess_files(cfgr, metadata, modulecfg) { 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)))); } }); files.push(cfgr.mountFile('/'+ get_game_name(game), cfgr.fetchFile("Game File", get_zip_url(game)))); files.push(cfgr.mountFile('/'+ modulecfg['driver'] + '.cfg', cfgr.fetchOptionalFile("CFG File", get_other_emulator_config_url(module)))); return files; } function get_mame_files(cfgr, metadata, modulecfg) { 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)))); } }); files.push(cfgr.mountFile('/'+ get_game_name(game), cfgr.fetchFile("Game File", get_zip_url(game)))); files.push(cfgr.mountFile('/'+ modulecfg['driver'] + '.cfg', cfgr.fetchOptionalFile("CFG File", get_other_emulator_config_url(module)))); 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 '//cors.archive.org/cors/jsmess_engine_v2/' + module + '.json'; }; var get_other_emulator_config_url = function (module) { return '//cors.archive.org/cors/jsmess_config_v2/' + module + '.cfg'; }; var get_meta_url = function (game_path) { var path = game_path.split('/'); return "//cors.archive.org/cors/"+ path[0] +"/"+ path[0] +"_meta.xml"; }; var get_zip_url = function (game_path) { return "//cors.archive.org/cors/"+ game_path; }; var get_js_url = function (js_filename) { return "//cors.archive.org/cors/jsmess_engine_v2/"+ js_filename; }; var get_bios_url = function (bios_filename) { return "//cors.archive.org/cors/jsmess_bios_v2/"+ bios_filename; }; function mountat (drive) { return function (data) { return { drive: drive, mountpoint: "/" + drive, data: data }; }; } return emulator; } 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.locateAdditionalEmulatorJS = function (func) { return { locateAdditionalJS: func }; }; 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 }; }; BaseLoader.fetchOptionalFile = function (title, url) { return { title: title, url: url, optional: true }; }; BaseLoader.localFile = function (title, data) { return { title: title, data: data }; }; function DosBoxLoader() { var config = Array.prototype.reduce.call(arguments, extend); config.emulator_arguments = build_dosbox_arguments(config.emulatorStart, config.files); return config; } DosBoxLoader.__proto__ = BaseLoader; DosBoxLoader.startExe = function (path) { return { emulatorStart: path }; }; function JSMESSLoader() { var config = Array.prototype.reduce.call(arguments, extend); config.emulator_arguments = build_mess_arguments(config.muted, config.mess_driver, config.nativeResolution, config.sample_rate, config.peripheral, config.extra_mess_args); config.needs_jsmess_webaudio = true; return config; } JSMESSLoader.__proto__ = BaseLoader; JSMESSLoader.driver = function (driver) { return { mess_driver: driver }; }; JSMESSLoader.peripheral = function (peripheral, game) { return { peripheral: [peripheral, game] }; }; JSMESSLoader.extraArgs = function (args) { return { extra_mess_args: args }; }; function JSMAMELoader() { var config = Array.prototype.reduce.call(arguments, extend); config.emulator_arguments = build_mame_arguments(config.muted, config.mess_driver, config.nativeResolution, config.sample_rate, config.extra_mess_args); config.needs_jsmess_webaudio = true; return config; } JSMAMELoader.__proto__ = BaseLoader; JSMAMELoader.driver = function (driver) { return { mess_driver: driver }; }; JSMAMELoader.extraArgs = function (args) { return { extra_mess_args: args }; }; var build_mess_arguments = function (muted, driver, native_resolution, sample_rate, peripheral, extra_args) { var args = [driver, '-verbose', '-rompath', 'emulator', '-window', '-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 && peripheral[0]) { args.push('-' + peripheral[0], '/emulator/'+ (peripheral[1].replace(/\//g,'_'))); } if (extra_args) { args = args.concat(extra_args); } return args; }; var build_mame_arguments = function (muted, driver, native_resolution, sample_rate, extra_args) { var args = [driver, '-verbose', '-rompath', 'emulator', '-window', '-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 (extra_args) { args = args.concat(extra_args); } return args; }; var build_dosbox_arguments = function (emulator_start, files) { var args = ['-conf', '/emulator/dosbox.conf']; var len = files.length; for (var i = 0; i < len; i++) { if ('mountpoint' in files[i]) { args.push('-c', 'mount '+ files[i].drive +' /emulator'+ files[i].mountpoint); } } 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; }; 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 splash = { loading_text: "", spinning: true, finished_loading: false, colors: { foreground: 'white', background: 'black' }, table: null, splashimg: new Image() }; setupSplash(canvas, splash); var SDL_PauseAudio; this.mute = function (state) { 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; }; 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/jsmess will set it to, but it avoids // the case where the size was left at the default 300x150 if (!canvas.hasAttribute("width")) { canvas.width = parseInt(getComputedStyle(canvas).width, 10); canvas.height = parseInt(getComputedStyle(canvas).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; if (typeof options !== 'object') { options = { waitAfterDownloading: false }; } var k, c, game_data; drawsplash(); var loading; if (typeof loadFiles === 'function') { loading = loadFiles(fetch_file, splash); } else { loading = Promise.resolve(loadFiles); } loading.then(function (_game_data) { game_data = _game_data; game_data.fs = new BrowserFS.FileSystem.MountableFileSystem(); 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; game_data.fs.mount(mountpoint, BFSOpenZip(new Buffer(data))); } }; } function saveat(filename) { return function (data) { if (data !== null) { game_data.fs.writeFileSync('/'+ filename, new Buffer(data), null, flag_w, 0x1a4); } }; } return Promise.all(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; })); }) .then(function (game_files) { 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 () { splash.setTitle("Failed to download game data!"); splash.failed_loading = true; }) .then(function () { 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); if (game_data.needs_jsmess_webaudio) setup_jsmess_webaudio(); // 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); Module = init_module(game_data.emulator_arguments, game_data.fs, game_data.locateAdditionalJS, game_data.nativeResolution, game_data.aspectRatio); if (callbacks && callbacks.before_emulator) { try { callbacks.before_emulator(); } catch (x) { console.log(x); } } if (game_data.emulatorJS) { splash.setTitle("Launching Emulator"); attach_script(game_data.emulatorJS); } else { splash.setTitle("Non-system disk or disk error"); } }, function () { splash.setTitle("Invalid media, track 0 bad or unusable"); splash.failed_loading = true; }); return this; }; this.start = start; var init_module = function(args, fs, locateAdditionalJS, nativeResolution, aspectRatio) { return { arguments: args, screenIsReadOnly: true, print: function (text) { console.log(text); }, canvas: canvas, noInitialRun: false, locateFile: locateAdditionalJS, preInit: function () { splash.setTitle("Loading game file(s) into file system"); // Re-initialize BFS to just use the writable in-memory storage. BrowserFS.initialize(fs); var BFS = new BrowserFS.EmscriptenFS(); // Mount the file system into Emscripten. FS.mkdir('/emulator'); FS.mount(BFS, {root: '/'}, '/emulator'); splash.finished_loading = true; splash.hide(); setTimeout(function () { resizeCanvas(canvas, scale = scale || scale, css_resolution = nativeResolution || css_resolution, aspectRatio = aspectRatio || aspectRatio); }); if (callbacks && callbacks.before_run) { window.setTimeout(function() { callbacks.before_run(); }, 0); } } }; }; 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 row = addRow(splash.table); var statusCell = row[0], titleCell = row[1], sizeCell = row[2]; titleCell.textContent = title; return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = rt ? rt : 'arraybuffer'; xhr.onprogress = function (e) { sizeCell.textContent = formatSize(e); }; xhr.onload = function (e) { sizeCell.textContent = formatSize(e); if (xhr.status === 200) { statusCell.textContent = '✔'; resolve(xhr.response); } }; xhr.onerror = function (e) { sizeCell.textContent = formatSize(e); if (optional) { statusCell.textContent = '?'; resolve(null); } else { statusCell.textContent = '✘'; reject(); } }; xhr.send(); }); }; function keyevent(resolve) { return function (e) { if (typeof loader_game === 'object') return; // game will start with click-to-play instead of [SPACE] char if (e.which == 32) { e.preventDefault(); resolve(); } }; }; var resizeCanvas = function (canvas, scale, resolution, aspectRatio) { if (scale && resolution) { canvas.style.imageRendering = 'optimizeSpeed'; 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.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 = "background" in splash.colors ? splash.colors.background : 'white'; context.fillRect(0, 0, canvas.width, canvas.height); console.log("canvas cleared"); }; function setupSplash(canvas, splash) { splash.splashElt = document.getElementById("emularity-splash-screen"); if (!splash.splashElt) { splash.splashElt = document.createElement('div'); splash.splashElt.setAttribute('id', "emularity-splash-screen"); splash.splashElt.style.position = 'absolute'; splash.splashElt.style.top = canvas.offsetTop +'px'; splash.splashElt.style.left = canvas.offsetLeft +'px'; splash.splashElt.style.width = canvas.offsetWidth +'px'; splash.splashElt.style.color = 'foreground' in splash.colors ? splash.colors.foreground : 'black'; splash.splashElt.style.backgroundColor = 'background' in splash.colors ? splash.colors.background : 'white'; canvas.parentElement.appendChild(splash.splashElt); } splash.splashimg.setAttribute('id', "emularity-splash-image"); 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.setAttribute('id', "emularity-splash-title"); 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); splash.spinnerimg.setAttribute('id', "emularity-spinner-image"); splash.spinnerimg.style.float = 'left'; splash.spinnerimg.style.margin = "1em"; splash.spinnerimg.setAttribute('width', "64px"); splash.spinnerimg.setAttribute('height', "64px"); splash.splashElt.appendChild(splash.spinnerimg); var table = document.getElementById("dosbox-progress-indicator"); if (!table) { table = document.createElement('table'); table.setAttribute('id', "dosbox-progress-indicator"); table.style.color = 'foreground' in splash.colors ? splash.colors.foreground : 'black'; table.style.backgroundColor = 'background' in splash.colors ? splash.colors.background : 'white'; splash.splashElt.appendChild(table); } splash.table = table; } splash.setTitle = function (title) { splash.titleElt.textContent = title; }; splash.hide = function () { splash.splashElt.style.display = 'none'; }; var addRow = function (table) { var row = table.insertRow(-1); var statusCell = row.insertCell(-1); statusCell.textContent = '—'; statusCell.style.width = "1.5em"; var titleCell = row.insertCell(-1); titleCell.textContent = '—'; titleCell.style.paddingRight = "1em"; var sizeCell = row.insertCell(-1); sizeCell.textContent = '—'; sizeCell.style.fontSize = "smaller"; return [statusCell, titleCell, sizeCell]; }; var drawsplash = function () { canvas.setAttribute('moz-opaque', ''); if (!splash.splashimg.src) { splash.splashimg.src = "logo/emularity_color_small.png"; } }; function attach_script(js_url) { if (js_url) { var head = document.getElementsByTagName('head')[0]; var newScript = document.createElement('script'); newScript.type = 'text/javascript'; newScript.src = js_url; head.appendChild(newScript); } } function getpointerlockenabler() { return canvas.requestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock; } function getfullscreenenabler() { return canvas.webkitRequestFullScreen || canvas.mozRequestFullScreen || canvas.requestFullScreen; } 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 () { Module.requestFullScreen(1, 0); }; /** * Prevents page navigation keys such as page up/page down from * moving the page while the user is playing. */ function blockSomeKeys() { var blocked_keys = [33, 34, 35, 36, 37, 38, 39, 40]; function keypress (e) { if (blocked_keys.indexOf(e.which) >= 0) { 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(); } }); } }; function BFSOpenZip(loadedData) { var zipfs = new BrowserFS.FileSystem.ZipFS(loadedData), mfs = new BrowserFS.FileSystem.MountableFileSystem(), memfs = new BrowserFS.FileSystem.InMemory(); mfs.mount('/zip', zipfs); mfs.mount('/mem', memfs); // Copy the read-only zip file contents to a writable in-memory storage. recursiveCopy(mfs, '/zip', '/mem'); return memfs; }; // 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; } }; // Helper function: Recursively copies contents from one folder to another. function recursiveCopy(fs, oldDir, newDir) { var path = BrowserFS.BFSRequire('path'); copyDirectory(oldDir, newDir); function copyDirectory(oldDir, newDir) { if (!fs.existsSync(newDir)) { fs.mkdirSync(newDir, 0777); } fs.readdirSync(oldDir).forEach(function(item) { var p = path.resolve(oldDir, item), newP = path.resolve(newDir, item); if (fs.statSync(p).isDirectory()) { copyDirectory(p, newP); } else { copyFile(p, newP); } }); } function copyFile(oldFile, newFile) { fs.writeFileSync(newFile, fs.readFileSync(oldFile, null, flag_r), null, flag_w, 0644); } }; /** * 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) { // 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(a[k], b[k]); }); return a; } return b; } window.IALoader = IALoader; window.DosBoxLoader = DosBoxLoader; window.JSMESSLoader = JSMESSLoader; window.JSMAMELoader = JSMAMELoader; window.Emulator = Emulator; })(typeof Promise === 'undefined' ? ES6Promise.Promise : Promise); // legacy var JSMESS = JSMESS || {}; JSMESS.ready = function (f) { f(); }; function setup_jsmess_webaudio() { // jsmess web audio backend v0.2 // katelyn gadd - kg at luminance dot org ; @antumbral on twitter var jsmess_web_audio = (function () { var context = null; var gain_node = null; var buffer_insert_point = null; var pending_buffers = []; var numChannels = 2; // constant in jsmess var sampleScale = 32766; var prebufferDuration = 100 / 1000; function lazy_init () { if (context || typeof AudioContext == 'undefined') return; context = new AudioContext(); gain_node = context.createGain(); gain_node.gain.value = 1.0; gain_node.connect(context.destination); }; function set_mastervolume ( // even though it's 'attenuation' the value is negative, so... attenuation_in_decibels ) { lazy_init(); if (!context) return; // http://stackoverflow.com/questions/22604500/web-audio-api-working-with-decibels // seemingly incorrect/broken. figures. welcome to Web Audio // var gain_web_audio = 1.0 - Math.pow(10, 10 / attenuation_in_decibels); // HACK: Max attenuation in JSMESS appears to be 32. // Hit ' then left/right arrow to test. // FIXME: This is linear instead of log10 scale. var gain_web_audio = 1.0 + (+attenuation_in_decibels / +32); if (gain_web_audio < +0) gain_web_audio = +0; else if (gain_web_audio > +1) gain_web_audio = +1; gain_node.gain.value = gain_web_audio; }; function update_audio_stream ( pBuffer, // pointer into emscripten heap. int16 samples samples_this_frame // int. number of samples at pBuffer address. ) { lazy_init(); if (!context) return; var buffer = context.createBuffer( numChannels, samples_this_frame, // JSMESS already initializes its mixer to use the context sampling rate. context.sampleRate ); for ( var channel_left = buffer.getChannelData(0), channel_right = buffer.getChannelData(1), i = 0, l = samples_this_frame | 0; i < l; i++ ) { var offset = // divide by sizeof(INT16) since pBuffer is offset // in bytes ((pBuffer / 2) | 0) + ((i * 2) | 0); var left_sample = HEAP16[offset]; var right_sample = HEAP16[(offset + 1) | 0]; // normalize from signed int16 to signed float var left_sample_float = left_sample / sampleScale; var right_sample_float = right_sample / sampleScale; channel_left[i] = left_sample_float; channel_right[i] = right_sample_float; } pending_buffers.push(buffer); tick(); }; function tick () { // Note: this is the time the web audio mixer has mixed up to, // not the actual current time. var now = context.currentTime; // prebuffering if (buffer_insert_point === null) { var total_buffered_seconds = 0; for (var i = 0, l = pending_buffers.length; i < l; i++) { var buffer = pending_buffers[i]; total_buffered_seconds += buffer.duration; } // Buffer not full enough? abort if (total_buffered_seconds < prebufferDuration) return; } // FIXME/TODO: It's possible for us to burn through the whole // chunk of prebuffered audio. At that point it seems like // JSMESS never catches up and our sound glitches forever. var insert_point = (buffer_insert_point === null) ? now : buffer_insert_point; if (pending_buffers.length) { for (var i = 0, l = pending_buffers.length; i < l; i++) { var buffer = pending_buffers[i]; var source_node = context.createBufferSource(); source_node.buffer = buffer; source_node.connect(gain_node); source_node.start(insert_point); insert_point += buffer.duration; } pending_buffers.length = 0; buffer_insert_point = insert_point; if (buffer_insert_point <= now) buffer_insert_point = now; } }; function get_context() { return context; }; return { set_mastervolume: set_mastervolume, update_audio_stream: update_audio_stream, get_context: get_context }; })(); window.jsmess_set_mastervolume = jsmess_web_audio.set_mastervolume; window.jsmess_update_audio_stream = jsmess_web_audio.update_audio_stream; window.jsmess_web_audio = jsmess_web_audio; }