diff --git a/emdosbox-launcher.js b/emdosbox-launcher.js index 35a114e..8260db5 100644 --- a/emdosbox-launcher.js +++ b/emdosbox-launcher.js @@ -1,7 +1,11 @@ var ar = new Array(33,34,35,36,37,38,39,40); function getfullscreenenabler() { - return canvas.webkitRequestFullScreen || canvas.mozRequestFullScreen || canvas.requestFullScreen; + return canvas.webkitRequestFullScreen || canvas.mozRequestFullScreen || canvas.requestFullScreen; +} + +function getpointerlockenabler() { + return canvas.requestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock; } function isfullscreensupported() { @@ -67,6 +71,12 @@ window.onkeydown = keypress; emulator = new DOSBOX(canvas).setscale(get('scale') ? parseFloat(get('scale')) : 1) .setmodule(module) .setgame(getgameurl(loader_game)); + 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(); + if (get('autostart')) { emulator.start(); } @@ -86,8 +96,22 @@ window.onkeydown = keypress; } function getgameurl(game) { + // 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 return (game === 'NONE') ? undefined - : ('//archive.org/cors/'+ game); + : ('//cors.archive.org/cors/'+ game); + } + + /** + * 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 switchgame(e) { diff --git a/emdosbox-loader.js b/emdosbox-loader.js index 6350408..b38dc15 100644 --- a/emdosbox-loader.js +++ b/emdosbox-loader.js @@ -1,6 +1,7 @@ var Module = null; function DOSBOX(canvas, module, game, precallback, callback, scale) { + var js_url; var moduledata; var requests = []; var drawloadingtimer; @@ -8,7 +9,9 @@ function DOSBOX(canvas, module, game, precallback, callback, scale) { var spinnerrot = 0; var splashimg = new Image(); var spinnerimg = new Image(); + // TODO: Have an enum value that communicates the current state of DOSBOX, e.g. 'initializing', 'loading', 'running'. var has_started = false; + var loading = false; var LOADING_TEXT; var SAMPLE_RATE = (function () { @@ -20,6 +23,10 @@ function DOSBOX(canvas, module, game, precallback, callback, scale) { return sample.sampleRate.toString(); }()); + var can_start = function () { + return !!canvas && !!module && !!game && !!scale && !has_started; + }; + this.setscale = function(_scale) { scale = _scale; try_start(); @@ -48,128 +55,10 @@ function DOSBOX(canvas, module, game, precallback, callback, scale) { return this; }; - try_start(); - - function try_start () { - if (!can_start()) { - return; - } - has_started = true; - var config = fetch_file('ModuleInfo', - '//archive.org/cors/jsmess_engine_v2/' + module + '.json', - 'text', true, true); - config.then(function (data) { - moduledata = data; - window.addEventListener('keypress', keyevent); - canvas.addEventListener('click', start); - drawsplash(); - }); - } - - function can_start() { - return !!canvas && !!module && !!game && !!scale && !has_started; - }; - - var keyevent = 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(); - start(); - } - }; - - var start = function() { - window.removeEventListener('keypress', keyevent); - canvas.removeEventListener('click', start); - drawloadingtimer = window.setInterval(draw_loading_status, 1000/60); - if (precallback) { - window.setTimeout(precallback, 0); - } - - LOADING_TEXT = 'Parsing config'; - var modulecfg = JSON.parse(moduledata); - - var nr = modulecfg['native_resolution']; - DOSBOX.width = nr[0] * scale; - DOSBOX.height = nr[1] * scale; - - file_countdown = 2; - - LOADING_TEXT = 'Downloading game data...'; - Promise.all([fetch_file('Metadata', - get_meta_url(game), - 'document', true), - fetch_file('Game', - game)]) - .then(function(game_data) { - LOADING_TEXT = 'Loading game file into file system'; - DOSBOX.BFSMountZip(new BrowserFS.BFSRequire('buffer').Buffer(game_file)); - Module = init_module(modulecfg, game_data[0], game_data[1]); - if (modulecfg['js_filename']) { - LOADING_TEXT = 'Launching DosBox'; - attach_script(modulecfg['js_filename']); - } else { - LOADING_TEXT = 'Invalid System Disk'; - } - handle_mute(); - }); - return this; - }; - this.start = start; - //global hook to method (so can be invoked with a "click to play" image being clicked) - window.DOSBOXstart = start; - - function init_module(modulecfg, meta_file, game_file) { - return { arguments: build_dosbox_arguments(modulecfg, - meta_file.getElementsByTagName("emulator_start") - .item(0) - .textContent), - screenIsReadOnly: true, - print: function (text) { console.log(text); }, - canvas: canvas, - noInitialRun: false, - preInit: function () { - window.clearInterval(drawloadingtimer); - if (callback) { - modulecfg.canvas = canvas; - window.setTimeout(function() { - callback(modulecfg); - }, - 0); - } - } - }; - } - - var drawsplash = function() { - var context = canvas.getContext('2d'); - splashimg.onload = function() { - context.clearRect(0, 0, canvas.width, canvas.height); - context.save(); - context.drawImage(splashimg, - canvas.width / 2 - (splashimg.width / 2), - canvas.height / 3 - (splashimg.height / 2)); - context.font = '18px sans-serif'; - context.fillStyle = 'Black'; - context.textAlign = 'center'; - context.fillText('Press the SPACEBAR to start.', - canvas.width / 2, - (canvas.height / 2) + (splashimg.height / 2)); - context.textAlign = 'start'; - context.restore(); - }; - spinnerimg.onload = function() { - splashimg.src = '/images/dosbox.png';; - }; - spinnerimg.src = '/images/spinner.png'; - }; - var draw_loading_status = function() { var context = canvas.getContext('2d'); context.clearRect(0, 0, canvas.width, canvas.height); - context.drawImage(splashimg, - canvas.width / 2 - (splashimg.width / 2), - canvas.height / 3 - (splashimg.height / 2)); + context.drawImage(splashimg, canvas.width / 2 - (splashimg.width / 2), canvas.height / 3 - (splashimg.height / 2)); var spinnerpos = (canvas.height / 2 + splashimg.height / 2) + 16; context.save(); context.translate((canvas.width / 2), spinnerpos); @@ -180,9 +69,7 @@ function DOSBOX(canvas, module, game, precallback, callback, scale) { context.font = '18px sans-serif'; context.fillStyle = 'Black'; context.textAlign = 'center'; - context.fillText(LOADING_TEXT, - canvas.width / 2, - (canvas.height / 2) + (splashimg.height / 4)); + context.fillText(LOADING_TEXT, canvas.width / 2, (canvas.height / 2) + (splashimg.height / 4)); context.restore(); spinnerrot += .25; }; @@ -196,36 +83,60 @@ function DOSBOX(canvas, module, game, precallback, callback, scale) { } }; - var fetch_file = function(title, url, rt, raw, unmanaged) { - return new Promise(function (resolve, reject) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = rt ? rt : 'arraybuffer'; - xhr.onload = function(e) { - if (xhr.status != 200) { - return; - } - if (!unmanaged) { - xhr.progress = 1.0; - } - resolve(raw ? xhr.response - : new Int8Array(xhr.response)); - }; - xhr.onerror = reject; - if (!unmanaged) { - xhr.onprogress = progress_fetch_file; - xhr.title = title; - xhr.progress = 0; - xhr.total = 0; - xhr.loaded = 0; - xhr.lengthComputable = false; - requests.push(xhr); - } - xhr.send(); - }); + var fetch_file = function(title, url, cb, rt, raw, unmanaged) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = rt ? rt : 'arraybuffer'; + xhr.onload = function(e) { + if (xhr.status != 200) { + return; + } + if (!unmanaged) { + xhr.progress = 1.0; + } + var ints = raw ? xhr.response : new Int8Array(xhr.response); + cb(ints); + }; + if (!unmanaged) { + xhr.onprogress = progress_fetch_file; + xhr.title = title; + xhr.progress = 0; + xhr.total = 0; + xhr.loaded = 0; + xhr.lengthComputable = false; + requests.push(xhr); + } + xhr.send(); + }; + + var update_countdown = function() { + file_countdown -= 1; + if (file_countdown <= 0) { + loading = false; + + if (js_url) { + var head = document.getElementsByTagName('head')[0]; + var newScript = document.createElement('script'); + newScript.type = 'text/javascript'; + newScript.src = get_js_url(js_url); + head.appendChild(newScript); + } + + // see archive.js for the mute/unmute button/JS + if (!($.cookie && $.cookie('unmute'))){ + setTimeout(function(){ + // someone moved it from 1st to 2nd! + if (DOSBOX && typeof(DOSBOX.sdl_pauseaudio)!='undefined') + DOSBOX.sdl_pauseaudio(1); + else if (typeof _SDL_PauseAudio !== "undefined") + _SDL_PauseAudio(1); + }, 3000); + } + } }; var build_dosbox_arguments = function (config, emulator_start) { + LOADING_TEXT = 'Building arguments'; return ['/dosprogram/'+ emulator_start]; }; @@ -235,33 +146,151 @@ function DOSBOX(canvas, module, game, precallback, callback, scale) { var get_meta_url = function (game_path) { var path = game_path.split('/'); - return "//archive.org/cors/"+ path[4] +"/"+ path[4] +"_meta.xml"; + // 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 + return "//cors.archive.org/cors/"+ path[4] +"/"+ path[4] +"_meta.xml"; }; var get_js_url = function (js_filename) { - return "//archive.org/cors/jsmess_engine_v2/"+ js_filename; + // 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 + return "//cors.archive.org/cors/jsmess_engine_v2/"+ js_filename; }; - function attach_script(url) { - var head = document.getElementsByTagName('head')[0]; - var newScript = document.createElement('script'); - newScript.type = 'text/javascript'; - newScript.src = get_js_url(url); - head.appendChild(newScript); + var init_module = function() { + LOADING_TEXT = 'Loading Program'; + var modulecfg = JSON.parse(moduledata); + js_url = modulecfg['js_filename']; + + var game_file = null, + meta_file = null; + + var nr = modulecfg['native_resolution']; + DOSBOX.width = nr[0] * scale; + DOSBOX.height = nr[1] * scale; + + // Makes the keyboard 'focusable' to let the canvas accept keyboard input. + // http://gamedev.stackexchange.com/questions/50223/receiving-keyboard-events-on-a-canvas-in-javascript + canvas.setAttribute('tabindex', '0'); + // Emscripten blocks the 'default action' of all mouse events, which + // prevents users from selecting the canvas for keyboard input! + // Prevent Emscripten from blocking users from selecting the canvas by + // manually 'focusing' the canvas when it is clicked. + canvas.addEventListener('mousedown', function() { + canvas.focus(); + }); + // Start the canvas focused. + canvas.focus(); + + Module = { + arguments: undefined, + screenIsReadOnly: true, + print: (function() { + return function(text) { + console.log(text); + }; + })(), + canvas: canvas, + // Prevent Emscripten from listening / blocking key events to the rest of the page by + // isolating keyboard input to the canvas. + keyboardListeningElement: canvas, + noInitialRun: false, + preInit: function() { + Module.arguments = build_dosbox_arguments(modulecfg, + meta_file.getElementsByTagName("emulator_start") + .item(0) + .textContent);; + LOADING_TEXT = 'Loading game file into file system'; + DOSBOX.BFSMountZip(game_file); + DOSBOX.moveConfigToRoot(); + window.clearInterval(drawloadingtimer); + if (callback) { + modulecfg.canvas = canvas; + window.setTimeout(function() { callback(modulecfg); }, 0); + } + } + }; + + file_countdown = 2; + + fetch_file('Metadata', + get_meta_url(game), + function(data) { + meta_file = data; + update_countdown(); + }, + 'document', true); + fetch_file('Game', + game, + function(data) { + game_file = new BrowserFS.BFSRequire('buffer').Buffer(data); + update_countdown(); + }); + }; + + var keyevent = 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(); + start(); + } + }; + + var start = function() { + // Prevent loading the game multiple times. + if (loading) { + return false; + } + window.removeEventListener('keypress', keyevent); + canvas.removeEventListener('click', start); + loading = true; + drawloadingtimer = window.setInterval(draw_loading_status, 1000/60); + if (precallback) { + window.setTimeout(precallback, 0); + } + init_module(); + return this; + }; + this.start = start; + window.DOSBOXstart = start;//global hook to method (so can be invoked with a "click to play" image being clicked) + + var drawsplash = function() { + var context = canvas.getContext('2d'); + splashimg.onload = function(){ + context.clearRect(0, 0, canvas.width, canvas.height); + context.save(); + context.drawImage(splashimg, canvas.width / 2 - (splashimg.width / 2), canvas.height / 3 - (splashimg.height / 2)); + context.font = '18px sans-serif'; + context.fillStyle = 'Black'; + context.textAlign = 'center'; + context.fillText('Click here to start', canvas.width / 2, (canvas.height / 2) + (splashimg.height / 2)); + context.textAlign = 'start'; + context.restore(); + }; + spinnerimg.onload = function() { + splashimg.src = '/images/dosbox.png';; + }; + spinnerimg.src = '/images/spinner.png'; + }; + + var configLoaded = function (data) { + moduledata = data; + window.addEventListener('keypress', keyevent); + canvas.addEventListener('click', start); + drawsplash(); + }; + + function try_start () { + if (!can_start()) { + return; + } + has_started = true; + // NOTE: deliberately use cors.archive.org since this will 302 rewrite to iaXXXXX.us.archive.org/XX/items/jsmess_engine_v2/...json + // and need to keep that "artificial" extra domain-ish name to avoid CORS issues with IE/Safari + fetch_file('ModuleInfo', '//cors.archive.org/cors/jsmess_engine_v2/' + module + '.json', configLoaded, 'text', true, true); } - function handle_mute() { - // see archive.js for the mute/unmute button/JS - if (!($.cookie && $.cookie('unmute'))){ - setTimeout(function(){ - // someone moved it from 1st to 2nd! - if (DOSBOX && typeof(DOSBOX.sdl_pauseaudio)!='undefined') - DOSBOX.sdl_pauseaudio(1); - else if (typeof _SDL_PauseAudio !== "undefined") - _SDL_PauseAudio(1); - }, 3000); - } - } + try_start(); } DOSBOX._readySet = false; @@ -347,3 +376,36 @@ DOSBOX.recursiveCopy = function recursiveCopy(oldDir, newDir) { fs.writeFileSync(newFile, fs.readFileSync(oldFile)); } }; + +/** + * Searches for dosbox.conf, and moves it to '/dosbox.conf' so dosbox uses it. + */ +DOSBOX.moveConfigToRoot = function moveConfigToRoot() { + if (typeof FS !== 'undefined') { + var dosboxConfPath = null; + // Recursively search for dosbox.conf. + function searchDirectory(dirPath) { + FS.readdir(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.stat(itemPath); + if (FS.isDir(itemStat.mode)) { + searchDirectory(itemPath); + } else if (item === 'dosbox.conf') { + dosboxConfPath = itemPath; + } + }); + } + searchDirectory('/'); + + if (dosboxConfPath !== null) { + FS.writeFile('/dosbox.conf', FS.readFile(dosboxConfPath), { encoding: 'binary' }); + } + } +}; diff --git a/jsmess-launcher.js b/jsmess-launcher.js new file mode 100644 index 0000000..cf44985 --- /dev/null +++ b/jsmess-launcher.js @@ -0,0 +1,168 @@ +var ar = new Array(33,34,35,36,37,38,39,40); + +function getfullscreenenabler() { + return canvas.webkitRequestFullScreen || canvas.mozRequestFullScreen || canvas.requestFullScreen; +} + +function isfullscreensupported() { + return !!(getfullscreenenabler()); +} + +function gofullscreen() { + Module.requestFullScreen(1,0); +} + +function keypress(e) { + if (typeof(loader_game)=='object' && !loader_game.started) + return true; // Don't ignore certain keys yet (until game started by "click to play") + + var key = e.which; + if($.inArray(key,ar) > -1) { + e.preventDefault(); //Don't let arrow, pg up/down, home, end affect page position + return false; + } + return true; +} + +window.onkeydown = keypress; + +(function() { + function get(name) { + if(typeof(loader_game)=='object') + return loader_game[name]; //alternate case where dont have CGI args to parse... + if(name=(new RegExp('[?&]'+encodeURIComponent(name)+'=([^&]*)')).exec(location.search)) { + return decodeURIComponent(name[1]); + } + } + + var games; + var mess; + var module; + + function getmodule() { + module = get('module'); + module = module ? module : 'test'; + } + + function init() { + getmodule(); + ready(); + } + + function ready() { + var fullscreenbutton = document.getElementById('gofullscreen') + if (isfullscreensupported()) { + fullscreenbutton.addEventListener('click', gofullscreen); + if ('onfullscreenchange' in document) { + document.addEventListener('fullscreenchange', JSMESS.fullScreenChangeHandler); + } else if ('onmozfullscreenchange' in document) { + document.addEventListener('mozfullscreenchange', JSMESS.fullScreenChangeHandler); + } else if ('onwebkitfullscreenchange' in document) { + document.addEventListener('webkitfullscreenchange', JSMESS.fullScreenChangeHandler); + } + } else { + fullscreenbutton.disabled = true; + } + var canvas = document.getElementById('canvas'); + mess = new JSMESS(canvas) + .setscale(get('scale') ? parseFloat(get('scale')) : 1) + .setmodule(module) + setgame(loader_game); + if (get('autostart')) { + mess.start(); + } + // Gamepad text + if (detectgamepadsupport()) { + var gamepadDiv = document.getElementById('gamepadtext'); + gamepadDiv.innerHTML = "No gamepads detected. Press a button on a gamepad to use it."; + listenforgamepads(function(gamepads, newgamepad) { + var s = (gamepads.length === 1 ? '' : 's'); + gamepadDiv.innerHTML = gamepads.length + ' gamepad'+s+' detected. If the game does not ' + + 'respond to your gamepad'+s+', refresh the browser and try again.'; + if (mess.hasStarted) { + gamepadDiv.innerHTML += "
Restart MESS to use new gamepads."; + } + }); + } + } + + function setgame(game) { + game = (game == 'NONE') ? undefined : game; + // 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 + mess.setgame(game ? '//cors.archive.org/cors/'+ game : undefined); + } + + function switchgame(e) { + setgame(e.target.value); + } + + // Firefox will not give us Joystick data unless we register this NOP + // callback. + // https://bugzilla.mozilla.org/show_bug.cgi?id=936104 + addEventListener("gamepadconnected", function() {}); + var getgamepads = navigator.getGamepads || navigator.webkitGamepads || + navigator.mozGamepads || navigator.gamepads || navigator.webkitGetGamepads; + /** + * Does the current browser support the Gamepad API? + * Returns a boolean. + */ + function detectgamepadsupport() { + return typeof getgamepads === 'function'; + } + // The timer that listens for gamepads, in case we ever want to stop it. + var gamepadlistener; + /** + * Listens for new gamepads, and triggers the callback when it detects a + * change. + * The callback is passed an array of active gamepads. + */ + function listenforgamepads(cb, freq) { + // NOP if the browser doesn't support gamepads. + if (!detectgamepadsupport()) return; + // Map from gamepad id to gamepad information. + var prevgamepads = {}; + // DEFAULT: Check gamepads every second. + if (typeof freq === 'undefined') freq = 1000; + gamepadlistener = setInterval(function() { + // Browsers get cranky when you don't apply this on the navigator object. + var gamepads = getgamepads.apply(navigator); + var currentgamepads = {}; + var i; + var hasChanged = false; + for (i = 0; i < gamepads.length; i++) { + var gamepad = gamepads[i]; + if (gamepad != null) { + currentgamepads[gamepad.id] = gamepad; + if (!prevgamepads.hasOwnProperty(gamepad.id)) { + // Gamepad has been added. + hasChanged = true; + } + } + } + + // Has a gamepad been removed? + if (!hasChanged) { + for (var gamepadid in prevgamepads) { + if (!currentgamepads.hasOwnProperty(gamepadid)) { + hasChanged = true; + } + } + } + + prevgamepads = currentgamepads; + + if (hasChanged) { + // Actual gamepads, filtered from gamepads. Chrome puts empty items into + // its gamepadlist. + var actualgamepads = []; + for (i = 0; i < gamepads.length; i++) { + if (gamepads[i] != null) actualgamepads.push(gamepads[i]); + } + cb(actualgamepads); + } + }, freq); + } + + window.addEventListener('load', init); +})(); \ No newline at end of file diff --git a/jsmess-loader.js b/jsmess-loader.js new file mode 100644 index 0000000..93211e6 --- /dev/null +++ b/jsmess-loader.js @@ -0,0 +1,394 @@ +var Module = null; + +function JSMESS(canvas, module, game, precallback, callback, scale) { + var js_data; + var moduledata; + var requests = []; + var drawloadingtimer; + var file_countdown; + var spinnerrot = 0; + var splashimg = new Image(); + var spinnerimg = new Image(); + var has_started = false; + var loading = false; + var LOADING_TEXT; + + 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 can_start = function () { + return !!canvas && !!module && !!game && !!scale && !has_started + }; + + this.setscale = function(_scale) { + scale = _scale; + try_start(); + return this; + } + + this.setprecallback = function(_precallback) { + precallback = _precallback; + return this; + } + + this.setcallback = function(_callback) { + callback = _callback; + return this; + } + + this.setmodule = function(_module) { + module = _module; + try_start(); + return this; + } + + this.setgame = function(_game) { + game = _game; + try_start(); + return this; + } + + var draw_loading_status = function() { + var context = canvas.getContext('2d'); + context.clearRect(0, 0, canvas.width, canvas.height); + context.drawImage(splashimg, canvas.width / 2 - (splashimg.width / 2), canvas.height / 3 - (splashimg.height / 2)); + var spinnerpos = (canvas.height / 2 + splashimg.height / 2) + 16; + context.save(); + context.translate((canvas.width / 2), spinnerpos); + context.rotate(spinnerrot); + context.drawImage(spinnerimg, -(64/2), -(64/2), 64, 64); + context.restore(); + context.save(); + context.font = '18px sans-serif'; + context.fillStyle = 'Black'; + context.textAlign = 'center'; + context.fillText(LOADING_TEXT, canvas.width / 2, (canvas.height / 2) + (splashimg.height / 4)); + context.restore(); + spinnerrot += .25; + }; + + var progress_fetch_file = function(e) { + if (e.lengthComputable) { + e.target.progress = e.loaded / e.total; + e.target.loaded = e.loaded; + e.target.total = e.total; + e.target.lengthComputable = e.lengthComputable; + } + }; + + var fetch_file = function(title, url, cb, rt, raw, unmanaged) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = rt ? rt : 'arraybuffer'; + xhr.onload = function(e) { + if (xhr.status != 200) { + return; + } + if (!unmanaged) { + xhr.progress = 1.0; + } + var ints = raw ? xhr.response : new Int8Array(xhr.response); + cb(ints); + }; + if (!unmanaged) { + xhr.onprogress = progress_fetch_file; + xhr.title = title; + xhr.progress = 0; + xhr.total = 0; + xhr.loaded = 0; + xhr.lengthComputable = false; + requests.push(xhr); + } + xhr.send(); + }; + + var update_countdown = function() { + file_countdown -= 1 + if (file_countdown <= 0) { + loading = false; + var headID = document.getElementsByTagName('head')[0]; + var newScript = document.createElement('script'); + newScript.type = 'text/javascript'; + newScript.text = js_data; + headID.appendChild(newScript); + + // see archive.js for the mute/unmute button/JS + if (!($.cookie && $.cookie('unmute'))){ + setTimeout(function(){ + // someone moved it from 1st to 2nd! + if (JSMESS && typeof(JSMESS.sdl_pauseaudio)!='undefined') + JSMESS.sdl_pauseaudio(1); + else if (_SDL_PauseAudio) + _SDL_PauseAudio(1); + + }, 3000); + } + } + }; + + var build_mess_arguments = function (config) { + LOADING_TEXT = 'Building arguments'; + var nr = config['native_resolution']; + // see archive.js for the mute/unmute button/JS + var muted = (!(typeof($.cookie)!='undefined' && $.cookie('unmute'))); + + var args = [ + config['driver'], + '-verbose', + '-rompath','.', + '-window', + '-resolution', nr[0]+'x' + nr[1], + '-nokeepaspect' + ]; + + if (config.autoboot) { + args.push('-autoboot_command'); + } + + if (muted){ + args.push('-sound', 'none'); + } else if (SAMPLE_RATE) { + args.push('-samplerate', SAMPLE_RATE); + } + + if (game) { + args.push('-' + config['peripherals'][0], game.replace(/\//g,'_')) + } + + if (config['extra_args']) { + args = args.concat(config['extra_args']) + } + + return args + }; + + var build_mame_arguments = function (config) { + LOADING_TEXT = 'Building arguments'; + var nr = config['native_resolution']; + // see archive.js for the mute/unmute button/JS + var muted = (!(typeof($.cookie)!='undefined' && $.cookie('unmute'))); + + var args = [ + config['driver'], + '-verbose', + '-rompath','.', + '-window', + '-resolution', nr[0]+'x' + nr[1], + '-nokeepaspect' + ]; + + if (muted){ + args.push('-sound', 'none'); + } else if (SAMPLE_RATE) { + args.push('-samplerate', SAMPLE_RATE); + } + + if (config['extra_args']) { + args = args.concat(config['extra_args']) + } + + return args + }; + + get_game_name = function (game_path) { + return game_path.split('/').pop(); + }; + + var init_module = function() { + LOADING_TEXT = 'Parsing config'; + var modulecfg = JSON.parse(moduledata); + + var game_file = null; + var keymap = null; + var bios_filenames = modulecfg['bios_filenames']; + var bios_files = {}; + + var nr = modulecfg['native_resolution']; + + JSMESS.width = nr[0] * scale; + JSMESS.height = nr[1] * scale; + + var use_mame = parseInt(modulecfg['arcade'], 10); + var arguments; + + if (use_mame) { + arguments = build_mame_arguments(modulecfg); + } else { + arguments = build_mess_arguments(modulecfg); + } + + Module = { + arguments: arguments, + screenIsReadOnly: true, + print: (function() { + return function(text) { + console.log(text); + }; + })(), + canvas: canvas, + noInitialRun: false, + preInit: function() { + LOADING_TEXT = 'Loading binary files into file system'; + // Load the downloaded binary files into the filesystem. + for (var bios_fname in bios_files) { + if (bios_files.hasOwnProperty(bios_fname)) { + Module['FS_createDataFile']('/', bios_fname, bios_files[bios_fname], true, true); + } + } + if (game && !use_mame) { + LOADING_TEXT = 'Loading game file into file system'; + Module['FS_createDataFile']('/', game.replace(/\//g,'_'), game_file, true, true); + } + Module['FS_createFolder']('/', 'cfg', true, true); + Module['FS_createDataFile']('/cfg', modulecfg['driver'] + '.cfg', keymap, true, true); + window.clearInterval(drawloadingtimer); + if (callback) { + modulecfg.canvas = canvas; + window.setTimeout(function() {callback(modulecfg)}, 0); + } + } + }; + + bios_filenames = bios_filenames.filter(String); + file_countdown = bios_filenames.length + (game ? 1 : 0) + 2 + + // Fetch the BIOS and the game we want to run. + LOADING_TEXT = 'Fetching BIOS and Game'; + if (!use_mame) { + for (var i=0; i < bios_filenames.length; i++) { + var fname = bios_filenames[i]; + // 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 + fetch_file('Bios', '//cors.archive.org/cors/jsmess_bios_v2/' + fname, function(data) { bios_files[fname] = data; update_countdown(); }); + } + } else { + fetch_file('Bios', game, function (data) { bios_files[get_game_name(game)] = data; update_countdown(); }); + } + + if (game && !use_mame) { + fetch_file('Game', game, function(data) { game_file = data; update_countdown(); }); + } + + // 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 + fetch_file('Keymap', '//cors.archive.org/cors/jsmess_config_v2/' + modulecfg['driver'] + '.cfg', function(data) { keymap = data; update_countdown(); }, 'text', true, true); + fetch_file('Javascript', '//cors.archive.org/cors/jsmess_engine_v2/' + modulecfg['js_filename'], function(data) { js_data = data; update_countdown(); }, 'text', true); + + }; + + var keyevent = 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(); + start(); + } + } + + var start = function() { + window.removeEventListener('keypress', keyevent); + canvas.removeEventListener('click', start); + loading = true; + drawloadingtimer = window.setInterval(draw_loading_status, 1000/60); + if (precallback) { + window.setTimeout(function() {precallback()}, 0); + } + init_module(); + return this; + } + this.start = start; + window.JSMESSstart = start;//global hook to method (so can be invoked with a "click to play" image being clicked) + + var drawsplash = function() { + var context = canvas.getContext('2d'); + splashimg.onload = function(){ + context.clearRect(0, 0, canvas.width, canvas.height); + context.save(); + context.drawImage(splashimg, canvas.width / 2 - (splashimg.width / 2), canvas.height / 3 - (splashimg.height / 2)); + context.font = '18px sans-serif'; + context.fillStyle = 'Black'; + context.textAlign = 'center'; + context.fillText('Click here to start', canvas.width / 2, (canvas.height / 2) + (splashimg.height / 2)); + context.textAlign = 'start'; + context.restore(); + }; + spinnerimg.onload = function() { + var use_mame = parseInt(JSON.parse(moduledata).arcade, 10); + var src; + if (use_mame) { + src = '/images/mame.png'; + } else { + src = '/images/mess.png'; + } + splashimg.src = src; + } + spinnerimg.src = '/images/spinner.png'; + } + + var configLoaded = function (data) { + moduledata = data; + window.addEventListener('keypress', keyevent); + canvas.addEventListener('click', start); + drawsplash(); + }; + + function try_start () { + if (!can_start()) { + return; + } + has_started = true; + // NOTE: deliberately use cors.archive.org since this will 302 rewrite to iaXXXXX.us.archive.org/XX/items/jsmess_engine_v2/...json + // and need to keep that "artificial" extra domain-ish name to avoid CORS issues with IE/Safari + fetch_file('ModuleInfo', '//cors.archive.org/cors/jsmess_engine_v2/' + module + '.json', configLoaded, 'text', true, true); + } + + try_start(); +} + +JSMESS._readySet = false; + +JSMESS._readyList = []; + +JSMESS._runReadies = function() { + if (JSMESS._readyList) { + for (var r=0; r < JSMESS._readyList.length; r++) { + JSMESS._readyList[r].call(window, []); + }; + JSMESS._readyList = []; + }; +}; + +JSMESS._readyCheck = function() { + if (JSMESS.running) { + JSMESS._runReadies(); + } else { + JSMESS._readySet = setTimeout(JSMESS._readyCheck, 10); + }; +}; + +JSMESS.ready = function(r) { + if (JSMESS.running) { + r.call(window, []); + } else { + JSMESS._readyList.push(function() { canvas.style.width = JSMESS.width + 'px'; canvas.style.height = JSMESS.height + 'px'; } ); + if (!(JSMESS._readySet)) { + JSMESS._readyCheck(); + } + }; +} + +JSMESS.setScale = function() { + Module.canvas.style.width = JSMESS.width + 'px'; + Module.canvas.style.height = JSMESS.height + 'px'; +}; + +JSMESS.fullScreenChangeHandler = function() { + if (!(document.mozFullScreenElement || document.fullScreenElement)) { + setTimeout(JSMESS.setScale, 0); + } +}