mirror of
https://github.com/db48x/emularity.git
synced 2026-09-15 02:52:57 +00:00
Merge branch 'promises'
This has been deployed to IA. Conflicts: emdosbox-launcher.js emdosbox-loader.js jsmess-launcher.js jsmess-loader.js
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
;;; Directory Local Variables
|
||||
;;; For more information see (info "(emacs) Directory Variables")
|
||||
|
||||
((js-mode
|
||||
(js2-basic-offset . 2)))
|
||||
@@ -0,0 +1,154 @@
|
||||
# Name #
|
||||
|
||||
js-emulators
|
||||
|
||||
# Synopsis #
|
||||
|
||||
The goal of this little project is to make it easy to embed a
|
||||
javascript-based emulator in your own webpage. It downloads the files
|
||||
you specify (with aprogress ui to show what is happening), arranges
|
||||
them to form a filesystem, constructs the necessary arguments for the
|
||||
emulator, handles transitions to and from full-screen mode, and
|
||||
detects and enables game pads.
|
||||
|
||||
To use this project you'll need to provide it with a canvas element,
|
||||
styled as necessary so that it has the correct size on screen (the
|
||||
program will be scaled up automatically to fit, controlling for aspect
|
||||
ratio). You will also likely want to provide a simple UI for entering
|
||||
full-screen mode or muting the audio; these can simply call methods on
|
||||
the emulator when activated.
|
||||
|
||||
# Emulator API #
|
||||
|
||||
The `Emulator` constructor takes three arguments: a canvas element, an
|
||||
optional callback (which will be called after fully initializing the
|
||||
emulator but just before it starts running the emulated program), and
|
||||
a config (as detailed below) or a function which returns a `Promise`
|
||||
of a config.
|
||||
|
||||
# Configuration #
|
||||
|
||||
## Examples ##
|
||||
|
||||
### Arcade game ###
|
||||
|
||||
Loads the emulator for the arcade game 1943, and gives it a compressed
|
||||
copy of the rom (which it loads from examples/1943.zip).
|
||||
|
||||
var emulator = new Emulator(document.querySelector("#canvas"),
|
||||
null,
|
||||
new JSMAMELoader(JSMAMELoader.driver("1943"),
|
||||
JSMAMELoader.nativeResolution(224, 256),
|
||||
JSMAMELoader.emulatorJS("emulators/mess1943.js"),
|
||||
JSMAMELoader.mountFile("1943.zip",
|
||||
JSMAMELoader.fetchFile("Game File",
|
||||
"examples/1943.zip"))))
|
||||
emulator.setScale(3);
|
||||
emulator.start({ waitAfterDownloading: true });
|
||||
|
||||
### Console game for Atari 2600 ###
|
||||
|
||||
Loads the emulator for the Atari 2600 console, and an image of a
|
||||
catridge for Pitfall. Notice how we download the image, storing it in
|
||||
a file, then set up a "cart" peripheral so that the emulator can find
|
||||
it. We also load a configuration file that preconfigures some
|
||||
keybindings needed to use the 2600.
|
||||
|
||||
var emulator = new Emulator(document.querySelector("#canvas"),
|
||||
null,
|
||||
new JSMESSLoader(JSMESSLoader.driver("a2600"),
|
||||
JSMESSLoader.nativeResolution(352, 223),
|
||||
JSMESSLoader.emulatorJS("emulators/messa2600.js"),
|
||||
JSMESSLoader.mountFile("Pitfall_Activision_1982.bin",
|
||||
JSMESSLoader.fetchFile("Game File",
|
||||
"examples/Pitfall_Activision_1982.bin")),
|
||||
JSMESSLoader.mountFile("a2600.cfg",
|
||||
JSMESSLoader.fetchFile("Config File",
|
||||
"examples/a2600.cfg")),
|
||||
JSMESSLoader.peripheral("cart", "Pitfall_Activision_1982.bin")))
|
||||
emulator.setScale(3).start({ waitAfterDownloading: true });
|
||||
|
||||
### DOS game ###
|
||||
|
||||
Here we load the dosbox emulator, and a zip file containing the game
|
||||
ZZT which we decompress and then mount as the C drive. We also tell
|
||||
DosBox to immediately start running zzt.exe, which is inside the zip.
|
||||
|
||||
var emulator = new Emulator(document.querySelector("#canvas"),
|
||||
null,
|
||||
new DosBoxLoader(DosBoxLoader.emulatorJS("emulators/dosbox.js"),
|
||||
DosBoxLoader.nativeResolution(640, 400),
|
||||
DosBoxLoader.mountZip("c",
|
||||
DosBoxLoader.fetchFile("Game File",
|
||||
"examples/Zzt_1991_Epic_Megagames_Inc.zip")),
|
||||
DosBoxLoader.startExe("zzt.exe")))
|
||||
emulator.start({ waitAfterDownloading: true });
|
||||
|
||||
## Configuration API ##
|
||||
|
||||
Currently there are two supported emulators, JSMESS and
|
||||
EM-DosBox. JSMESS provides emulation for arcade games, consoles, and
|
||||
early personal computers. As this emulator supports such a wide
|
||||
variety of hardware it has been broken up into several dozen emulators
|
||||
each supporting one machine lest the resulting javascript be
|
||||
intractably large (60+ megabytes). EM-DosBox provides emulation for
|
||||
software that runs on x86 PCs using the DOS operating systems common
|
||||
to the era.
|
||||
|
||||
Each of these is configured by calling a constructor function and
|
||||
providing it with arguments formed by calling static methods on that
|
||||
same constructor.
|
||||
|
||||
### Common ###
|
||||
|
||||
* `emulatorJS(url)`
|
||||
* `mountZip(drive, file)`
|
||||
* `mountFile(filename, file)`
|
||||
* `fetchFile(url)`
|
||||
* `fetchOptionalFile(url)`
|
||||
* `localFile(data)`
|
||||
|
||||
### JSMESS ###
|
||||
|
||||
* `driver(driverName)`
|
||||
* `extraArgs(args)`
|
||||
* `peripheral(name, filename)`
|
||||
|
||||
### JSMAME ###
|
||||
|
||||
* `driver(driverName)`
|
||||
* `extraArgs(args)`
|
||||
|
||||
### EM-DosBox ###
|
||||
|
||||
* `startExe(filename)`
|
||||
|
||||
## Internet Archive ##
|
||||
|
||||
There's also a helper for loading software from
|
||||
[the Internet Archive](https://archive.org/v2), `IALoader`. IALoader
|
||||
looks at the metadata associated with an Internet Archive item and
|
||||
uses that to build the configuration for the emulator.
|
||||
|
||||
## Examples ##
|
||||
|
||||
var emulator = new IALoader(document.querySelector("#canvas"),
|
||||
"Pitfall_Activision_1982/Pitfall_Activision_1982.bin");
|
||||
|
||||
# Runtime API #
|
||||
|
||||
Once you have an emulator object, there are several methods you can call.
|
||||
|
||||
* `start()`
|
||||
* `requestFullScreen()`
|
||||
* `mute()`
|
||||
* `setSplashColors()`
|
||||
* others…
|
||||
|
||||
# Known Bugs #
|
||||
|
||||
* splash screen doesn't always fit inside the canvas
|
||||
* need to improve the download progress indicators
|
||||
* browser feature detection for volume/mute/full-screen
|
||||
* handling of aspect ratios, and their interaction with full-screen mode
|
||||
* finish API for volume/mute/full-screen requests
|
||||
File diff suppressed because one or more lines are too long
@@ -1,191 +0,0 @@
|
||||
var ar = new Array(33,34,35,36,37,38,39,40);
|
||||
|
||||
function getfullscreenenabler() {
|
||||
return canvas.webkitRequestFullScreen || canvas.mozRequestFullScreen || canvas.requestFullScreen;
|
||||
}
|
||||
|
||||
function getpointerlockenabler() {
|
||||
return canvas.requestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock;
|
||||
}
|
||||
|
||||
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 emulator;
|
||||
var module;
|
||||
|
||||
function getmodule() {
|
||||
module = get('module');
|
||||
module = module ? module : 'test';
|
||||
}
|
||||
|
||||
function init() {
|
||||
getmodule();
|
||||
ready();
|
||||
}
|
||||
|
||||
function ready() {
|
||||
var fullscreenbutton = document.getElementById('gofullscreen');
|
||||
if (fullscreenbutton) {
|
||||
if (isfullscreensupported()) {
|
||||
fullscreenbutton.addEventListener('click', gofullscreen);
|
||||
if ('onfullscreenchange' in document) {
|
||||
document.addEventListener('fullscreenchange', DOSBOX.fullScreenChangeHandler);
|
||||
} else if ('onmozfullscreenchange' in document) {
|
||||
document.addEventListener('mozfullscreenchange', DOSBOX.fullScreenChangeHandler);
|
||||
} else if ('onwebkitfullscreenchange' in document) {
|
||||
document.addEventListener('webkitfullscreenchange', DOSBOX.fullScreenChangeHandler);
|
||||
}
|
||||
} else {
|
||||
fullscreenbutton.disabled = true;
|
||||
}
|
||||
}
|
||||
var canvas = document.getElementById('canvas');
|
||||
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();
|
||||
}
|
||||
// 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 (emulator.hasStarted) {
|
||||
gamepadDiv.innerHTML += "<br />Restart MESS to use new gamepads.";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
: ('//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) {
|
||||
emulator.setgame(getgameurl(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);
|
||||
})();
|
||||
@@ -1,437 +0,0 @@
|
||||
var Module = null;
|
||||
|
||||
function DOSBOX(canvas, module, game, precallback, callback, scale) {
|
||||
var js_url;
|
||||
var moduledata;
|
||||
var requests = [];
|
||||
var drawloadingtimer;
|
||||
var file_countdown;
|
||||
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 splash_inverse = getComputedStyle(document.getElementsByTagName("body")[0]).backgroundColor === 'rgb(0, 0, 0)';
|
||||
|
||||
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();
|
||||
}());
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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 = splash_inverse ? 'white' : '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;
|
||||
|
||||
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];
|
||||
};
|
||||
|
||||
var get_game_name = function (game_path) {
|
||||
return game_path.split('/').pop();
|
||||
};
|
||||
|
||||
var get_meta_url = function (game_path) {
|
||||
var path = game_path.split('/');
|
||||
// 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) {
|
||||
// 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;
|
||||
};
|
||||
|
||||
var init_module = function() {
|
||||
if (moduledata == null) {
|
||||
// HACK: Module data isn't ready yet. It'll call us once loaded.
|
||||
return;
|
||||
}
|
||||
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,
|
||||
locateFile: function (file) {
|
||||
if ("file_locations" in modulecfg && file in modulecfg.file_locations) {
|
||||
return get_js_url(modulecfg.file_locations[file]);
|
||||
}
|
||||
throw new Error("Don't know how to find file: "+ file);
|
||||
},
|
||||
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 = splash_inverse ? 'white' : '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();
|
||||
if (loading) {
|
||||
// HACK: User clicked play before module metadata loaded, and play aborted.
|
||||
// Now that metadata is ready, begin playing.
|
||||
init_module();
|
||||
}
|
||||
};
|
||||
|
||||
function try_start () {
|
||||
if (!can_start()) {
|
||||
return;
|
||||
}
|
||||
LOADING_TEXT = "Fetching item metadata...";
|
||||
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();
|
||||
}
|
||||
|
||||
DOSBOX._readySet = false;
|
||||
|
||||
DOSBOX._readyList = [];
|
||||
|
||||
DOSBOX._runReadies = function() {
|
||||
if (DOSBOX._readyList) {
|
||||
for (var r=0; r < DOSBOX._readyList.length; r++) {
|
||||
DOSBOX._readyList[r].call(window, []);
|
||||
};
|
||||
DOSBOX._readyList = [];
|
||||
};
|
||||
};
|
||||
|
||||
DOSBOX._readyCheck = function() {
|
||||
if (DOSBOX.running) {
|
||||
DOSBOX._runReadies();
|
||||
} else {
|
||||
DOSBOX._readySet = setTimeout(DOSBOX._readyCheck, 10);
|
||||
};
|
||||
};
|
||||
|
||||
DOSBOX.ready = function(r) {
|
||||
if (DOSBOX.running) {
|
||||
r.call(window, []);
|
||||
} else {
|
||||
DOSBOX._readyList.push(function() { canvas.style.width = DOSBOX.width + 'px'; canvas.style.height = DOSBOX.height + 'px'; } );
|
||||
if (!(DOSBOX._readySet)) {
|
||||
DOSBOX._readyCheck();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
DOSBOX.setScale = function() {
|
||||
Module.canvas.style.width = DOSBOX.width + 'px';
|
||||
Module.canvas.style.height = DOSBOX.height + 'px';
|
||||
};
|
||||
|
||||
DOSBOX.fullScreenChangeHandler = function() {
|
||||
if (!(document.mozFullScreenElement || document.fullScreenElement)) {
|
||||
setTimeout(DOSBOX.setScale, 0);
|
||||
}
|
||||
};
|
||||
|
||||
DOSBOX.BFSMountZip = function BFSMount(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);
|
||||
BrowserFS.initialize(mfs);
|
||||
// Copy the read-only zip file contents to a writable in-memory storage.
|
||||
this.recursiveCopy('/zip', '/mem');
|
||||
// Re-initialize BFS to just use the writable in-memory storage.
|
||||
BrowserFS.initialize(memfs);
|
||||
// Mount the file system into Emscripten.
|
||||
var BFS = new BrowserFS.EmscriptenFS();
|
||||
FS.mkdir('/dosprogram');
|
||||
FS.mount(BFS, {root: '/'}, '/dosprogram');
|
||||
};
|
||||
|
||||
// Helper function: Recursively copies contents from one folder to another.
|
||||
DOSBOX.recursiveCopy = function recursiveCopy(oldDir, newDir) {
|
||||
var path = BrowserFS.BFSRequire('path'),
|
||||
fs = BrowserFS.BFSRequire('fs');
|
||||
copyDirectory(oldDir, newDir);
|
||||
function copyDirectory(oldDir, newDir) {
|
||||
if (!fs.existsSync(newDir)) {
|
||||
fs.mkdirSync(newDir);
|
||||
}
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
+960
@@ -0,0 +1,960 @@
|
||||
/*!
|
||||
* @overview es6-promise - a tiny implementation of Promises/A+.
|
||||
* @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
|
||||
* @license Licensed under MIT license
|
||||
* See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
function $$utils$$objectOrFunction(x) {
|
||||
return typeof x === 'function' || (typeof x === 'object' && x !== null);
|
||||
}
|
||||
|
||||
function $$utils$$isFunction(x) {
|
||||
return typeof x === 'function';
|
||||
}
|
||||
|
||||
function $$utils$$isMaybeThenable(x) {
|
||||
return typeof x === 'object' && x !== null;
|
||||
}
|
||||
|
||||
var $$utils$$_isArray;
|
||||
|
||||
if (!Array.isArray) {
|
||||
$$utils$$_isArray = function (x) {
|
||||
return Object.prototype.toString.call(x) === '[object Array]';
|
||||
};
|
||||
} else {
|
||||
$$utils$$_isArray = Array.isArray;
|
||||
}
|
||||
|
||||
var $$utils$$isArray = $$utils$$_isArray;
|
||||
var $$utils$$now = Date.now || function() { return new Date().getTime(); };
|
||||
function $$utils$$F() { }
|
||||
|
||||
var $$utils$$o_create = (Object.create || function (o) {
|
||||
if (arguments.length > 1) {
|
||||
throw new Error('Second argument not supported');
|
||||
}
|
||||
if (typeof o !== 'object') {
|
||||
throw new TypeError('Argument must be an object');
|
||||
}
|
||||
$$utils$$F.prototype = o;
|
||||
return new $$utils$$F();
|
||||
});
|
||||
|
||||
var $$asap$$len = 0;
|
||||
|
||||
var $$asap$$default = function asap(callback, arg) {
|
||||
$$asap$$queue[$$asap$$len] = callback;
|
||||
$$asap$$queue[$$asap$$len + 1] = arg;
|
||||
$$asap$$len += 2;
|
||||
if ($$asap$$len === 2) {
|
||||
// If len is 1, that means that we need to schedule an async flush.
|
||||
// If additional callbacks are queued before the queue is flushed, they
|
||||
// will be processed by this flush that we are scheduling.
|
||||
$$asap$$scheduleFlush();
|
||||
}
|
||||
};
|
||||
|
||||
var $$asap$$browserGlobal = (typeof window !== 'undefined') ? window : {};
|
||||
var $$asap$$BrowserMutationObserver = $$asap$$browserGlobal.MutationObserver || $$asap$$browserGlobal.WebKitMutationObserver;
|
||||
|
||||
// test for web worker but not in IE10
|
||||
var $$asap$$isWorker = typeof Uint8ClampedArray !== 'undefined' &&
|
||||
typeof importScripts !== 'undefined' &&
|
||||
typeof MessageChannel !== 'undefined';
|
||||
|
||||
// node
|
||||
function $$asap$$useNextTick() {
|
||||
return function() {
|
||||
process.nextTick($$asap$$flush);
|
||||
};
|
||||
}
|
||||
|
||||
function $$asap$$useMutationObserver() {
|
||||
var iterations = 0;
|
||||
var observer = new $$asap$$BrowserMutationObserver($$asap$$flush);
|
||||
var node = document.createTextNode('');
|
||||
observer.observe(node, { characterData: true });
|
||||
|
||||
return function() {
|
||||
node.data = (iterations = ++iterations % 2);
|
||||
};
|
||||
}
|
||||
|
||||
// web worker
|
||||
function $$asap$$useMessageChannel() {
|
||||
var channel = new MessageChannel();
|
||||
channel.port1.onmessage = $$asap$$flush;
|
||||
return function () {
|
||||
channel.port2.postMessage(0);
|
||||
};
|
||||
}
|
||||
|
||||
function $$asap$$useSetTimeout() {
|
||||
return function() {
|
||||
setTimeout($$asap$$flush, 1);
|
||||
};
|
||||
}
|
||||
|
||||
var $$asap$$queue = new Array(1000);
|
||||
|
||||
function $$asap$$flush() {
|
||||
for (var i = 0; i < $$asap$$len; i+=2) {
|
||||
var callback = $$asap$$queue[i];
|
||||
var arg = $$asap$$queue[i+1];
|
||||
|
||||
callback(arg);
|
||||
|
||||
$$asap$$queue[i] = undefined;
|
||||
$$asap$$queue[i+1] = undefined;
|
||||
}
|
||||
|
||||
$$asap$$len = 0;
|
||||
}
|
||||
|
||||
var $$asap$$scheduleFlush;
|
||||
|
||||
// Decide what async method to use to triggering processing of queued callbacks:
|
||||
if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') {
|
||||
$$asap$$scheduleFlush = $$asap$$useNextTick();
|
||||
} else if ($$asap$$BrowserMutationObserver) {
|
||||
$$asap$$scheduleFlush = $$asap$$useMutationObserver();
|
||||
} else if ($$asap$$isWorker) {
|
||||
$$asap$$scheduleFlush = $$asap$$useMessageChannel();
|
||||
} else {
|
||||
$$asap$$scheduleFlush = $$asap$$useSetTimeout();
|
||||
}
|
||||
|
||||
function $$$internal$$noop() {}
|
||||
var $$$internal$$PENDING = void 0;
|
||||
var $$$internal$$FULFILLED = 1;
|
||||
var $$$internal$$REJECTED = 2;
|
||||
var $$$internal$$GET_THEN_ERROR = new $$$internal$$ErrorObject();
|
||||
|
||||
function $$$internal$$selfFullfillment() {
|
||||
return new TypeError("You cannot resolve a promise with itself");
|
||||
}
|
||||
|
||||
function $$$internal$$cannotReturnOwn() {
|
||||
return new TypeError('A promises callback cannot return that same promise.')
|
||||
}
|
||||
|
||||
function $$$internal$$getThen(promise) {
|
||||
try {
|
||||
return promise.then;
|
||||
} catch(error) {
|
||||
$$$internal$$GET_THEN_ERROR.error = error;
|
||||
return $$$internal$$GET_THEN_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
function $$$internal$$tryThen(then, value, fulfillmentHandler, rejectionHandler) {
|
||||
try {
|
||||
then.call(value, fulfillmentHandler, rejectionHandler);
|
||||
} catch(e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
function $$$internal$$handleForeignThenable(promise, thenable, then) {
|
||||
$$asap$$default(function(promise) {
|
||||
var sealed = false;
|
||||
var error = $$$internal$$tryThen(then, thenable, function(value) {
|
||||
if (sealed) { return; }
|
||||
sealed = true;
|
||||
if (thenable !== value) {
|
||||
$$$internal$$resolve(promise, value);
|
||||
} else {
|
||||
$$$internal$$fulfill(promise, value);
|
||||
}
|
||||
}, function(reason) {
|
||||
if (sealed) { return; }
|
||||
sealed = true;
|
||||
|
||||
$$$internal$$reject(promise, reason);
|
||||
}, 'Settle: ' + (promise._label || ' unknown promise'));
|
||||
|
||||
if (!sealed && error) {
|
||||
sealed = true;
|
||||
$$$internal$$reject(promise, error);
|
||||
}
|
||||
}, promise);
|
||||
}
|
||||
|
||||
function $$$internal$$handleOwnThenable(promise, thenable) {
|
||||
if (thenable._state === $$$internal$$FULFILLED) {
|
||||
$$$internal$$fulfill(promise, thenable._result);
|
||||
} else if (promise._state === $$$internal$$REJECTED) {
|
||||
$$$internal$$reject(promise, thenable._result);
|
||||
} else {
|
||||
$$$internal$$subscribe(thenable, undefined, function(value) {
|
||||
$$$internal$$resolve(promise, value);
|
||||
}, function(reason) {
|
||||
$$$internal$$reject(promise, reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function $$$internal$$handleMaybeThenable(promise, maybeThenable) {
|
||||
if (maybeThenable.constructor === promise.constructor) {
|
||||
$$$internal$$handleOwnThenable(promise, maybeThenable);
|
||||
} else {
|
||||
var then = $$$internal$$getThen(maybeThenable);
|
||||
|
||||
if (then === $$$internal$$GET_THEN_ERROR) {
|
||||
$$$internal$$reject(promise, $$$internal$$GET_THEN_ERROR.error);
|
||||
} else if (then === undefined) {
|
||||
$$$internal$$fulfill(promise, maybeThenable);
|
||||
} else if ($$utils$$isFunction(then)) {
|
||||
$$$internal$$handleForeignThenable(promise, maybeThenable, then);
|
||||
} else {
|
||||
$$$internal$$fulfill(promise, maybeThenable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function $$$internal$$resolve(promise, value) {
|
||||
if (promise === value) {
|
||||
$$$internal$$reject(promise, $$$internal$$selfFullfillment());
|
||||
} else if ($$utils$$objectOrFunction(value)) {
|
||||
$$$internal$$handleMaybeThenable(promise, value);
|
||||
} else {
|
||||
$$$internal$$fulfill(promise, value);
|
||||
}
|
||||
}
|
||||
|
||||
function $$$internal$$publishRejection(promise) {
|
||||
if (promise._onerror) {
|
||||
promise._onerror(promise._result);
|
||||
}
|
||||
|
||||
$$$internal$$publish(promise);
|
||||
}
|
||||
|
||||
function $$$internal$$fulfill(promise, value) {
|
||||
if (promise._state !== $$$internal$$PENDING) { return; }
|
||||
|
||||
promise._result = value;
|
||||
promise._state = $$$internal$$FULFILLED;
|
||||
|
||||
if (promise._subscribers.length === 0) {
|
||||
} else {
|
||||
$$asap$$default($$$internal$$publish, promise);
|
||||
}
|
||||
}
|
||||
|
||||
function $$$internal$$reject(promise, reason) {
|
||||
if (promise._state !== $$$internal$$PENDING) { return; }
|
||||
promise._state = $$$internal$$REJECTED;
|
||||
promise._result = reason;
|
||||
|
||||
$$asap$$default($$$internal$$publishRejection, promise);
|
||||
}
|
||||
|
||||
function $$$internal$$subscribe(parent, child, onFulfillment, onRejection) {
|
||||
var subscribers = parent._subscribers;
|
||||
var length = subscribers.length;
|
||||
|
||||
parent._onerror = null;
|
||||
|
||||
subscribers[length] = child;
|
||||
subscribers[length + $$$internal$$FULFILLED] = onFulfillment;
|
||||
subscribers[length + $$$internal$$REJECTED] = onRejection;
|
||||
|
||||
if (length === 0 && parent._state) {
|
||||
$$asap$$default($$$internal$$publish, parent);
|
||||
}
|
||||
}
|
||||
|
||||
function $$$internal$$publish(promise) {
|
||||
var subscribers = promise._subscribers;
|
||||
var settled = promise._state;
|
||||
|
||||
if (subscribers.length === 0) { return; }
|
||||
|
||||
var child, callback, detail = promise._result;
|
||||
|
||||
for (var i = 0; i < subscribers.length; i += 3) {
|
||||
child = subscribers[i];
|
||||
callback = subscribers[i + settled];
|
||||
|
||||
if (child) {
|
||||
$$$internal$$invokeCallback(settled, child, callback, detail);
|
||||
} else {
|
||||
callback(detail);
|
||||
}
|
||||
}
|
||||
|
||||
promise._subscribers.length = 0;
|
||||
}
|
||||
|
||||
function $$$internal$$ErrorObject() {
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
var $$$internal$$TRY_CATCH_ERROR = new $$$internal$$ErrorObject();
|
||||
|
||||
function $$$internal$$tryCatch(callback, detail) {
|
||||
try {
|
||||
return callback(detail);
|
||||
} catch(e) {
|
||||
$$$internal$$TRY_CATCH_ERROR.error = e;
|
||||
return $$$internal$$TRY_CATCH_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
function $$$internal$$invokeCallback(settled, promise, callback, detail) {
|
||||
var hasCallback = $$utils$$isFunction(callback),
|
||||
value, error, succeeded, failed;
|
||||
|
||||
if (hasCallback) {
|
||||
value = $$$internal$$tryCatch(callback, detail);
|
||||
|
||||
if (value === $$$internal$$TRY_CATCH_ERROR) {
|
||||
failed = true;
|
||||
error = value.error;
|
||||
value = null;
|
||||
} else {
|
||||
succeeded = true;
|
||||
}
|
||||
|
||||
if (promise === value) {
|
||||
$$$internal$$reject(promise, $$$internal$$cannotReturnOwn());
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
value = detail;
|
||||
succeeded = true;
|
||||
}
|
||||
|
||||
if (promise._state !== $$$internal$$PENDING) {
|
||||
// noop
|
||||
} else if (hasCallback && succeeded) {
|
||||
$$$internal$$resolve(promise, value);
|
||||
} else if (failed) {
|
||||
$$$internal$$reject(promise, error);
|
||||
} else if (settled === $$$internal$$FULFILLED) {
|
||||
$$$internal$$fulfill(promise, value);
|
||||
} else if (settled === $$$internal$$REJECTED) {
|
||||
$$$internal$$reject(promise, value);
|
||||
}
|
||||
}
|
||||
|
||||
function $$$internal$$initializePromise(promise, resolver) {
|
||||
try {
|
||||
resolver(function resolvePromise(value){
|
||||
$$$internal$$resolve(promise, value);
|
||||
}, function rejectPromise(reason) {
|
||||
$$$internal$$reject(promise, reason);
|
||||
});
|
||||
} catch(e) {
|
||||
$$$internal$$reject(promise, e);
|
||||
}
|
||||
}
|
||||
|
||||
function $$$enumerator$$makeSettledResult(state, position, value) {
|
||||
if (state === $$$internal$$FULFILLED) {
|
||||
return {
|
||||
state: 'fulfilled',
|
||||
value: value
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
state: 'rejected',
|
||||
reason: value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function $$$enumerator$$Enumerator(Constructor, input, abortOnReject, label) {
|
||||
this._instanceConstructor = Constructor;
|
||||
this.promise = new Constructor($$$internal$$noop, label);
|
||||
this._abortOnReject = abortOnReject;
|
||||
|
||||
if (this._validateInput(input)) {
|
||||
this._input = input;
|
||||
this.length = input.length;
|
||||
this._remaining = input.length;
|
||||
|
||||
this._init();
|
||||
|
||||
if (this.length === 0) {
|
||||
$$$internal$$fulfill(this.promise, this._result);
|
||||
} else {
|
||||
this.length = this.length || 0;
|
||||
this._enumerate();
|
||||
if (this._remaining === 0) {
|
||||
$$$internal$$fulfill(this.promise, this._result);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$$$internal$$reject(this.promise, this._validationError());
|
||||
}
|
||||
}
|
||||
|
||||
$$$enumerator$$Enumerator.prototype._validateInput = function(input) {
|
||||
return $$utils$$isArray(input);
|
||||
};
|
||||
|
||||
$$$enumerator$$Enumerator.prototype._validationError = function() {
|
||||
return new Error('Array Methods must be provided an Array');
|
||||
};
|
||||
|
||||
$$$enumerator$$Enumerator.prototype._init = function() {
|
||||
this._result = new Array(this.length);
|
||||
};
|
||||
|
||||
var $$$enumerator$$default = $$$enumerator$$Enumerator;
|
||||
|
||||
$$$enumerator$$Enumerator.prototype._enumerate = function() {
|
||||
var length = this.length;
|
||||
var promise = this.promise;
|
||||
var input = this._input;
|
||||
|
||||
for (var i = 0; promise._state === $$$internal$$PENDING && i < length; i++) {
|
||||
this._eachEntry(input[i], i);
|
||||
}
|
||||
};
|
||||
|
||||
$$$enumerator$$Enumerator.prototype._eachEntry = function(entry, i) {
|
||||
var c = this._instanceConstructor;
|
||||
if ($$utils$$isMaybeThenable(entry)) {
|
||||
if (entry.constructor === c && entry._state !== $$$internal$$PENDING) {
|
||||
entry._onerror = null;
|
||||
this._settledAt(entry._state, i, entry._result);
|
||||
} else {
|
||||
this._willSettleAt(c.resolve(entry), i);
|
||||
}
|
||||
} else {
|
||||
this._remaining--;
|
||||
this._result[i] = this._makeResult($$$internal$$FULFILLED, i, entry);
|
||||
}
|
||||
};
|
||||
|
||||
$$$enumerator$$Enumerator.prototype._settledAt = function(state, i, value) {
|
||||
var promise = this.promise;
|
||||
|
||||
if (promise._state === $$$internal$$PENDING) {
|
||||
this._remaining--;
|
||||
|
||||
if (this._abortOnReject && state === $$$internal$$REJECTED) {
|
||||
$$$internal$$reject(promise, value);
|
||||
} else {
|
||||
this._result[i] = this._makeResult(state, i, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._remaining === 0) {
|
||||
$$$internal$$fulfill(promise, this._result);
|
||||
}
|
||||
};
|
||||
|
||||
$$$enumerator$$Enumerator.prototype._makeResult = function(state, i, value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
$$$enumerator$$Enumerator.prototype._willSettleAt = function(promise, i) {
|
||||
var enumerator = this;
|
||||
|
||||
$$$internal$$subscribe(promise, undefined, function(value) {
|
||||
enumerator._settledAt($$$internal$$FULFILLED, i, value);
|
||||
}, function(reason) {
|
||||
enumerator._settledAt($$$internal$$REJECTED, i, reason);
|
||||
});
|
||||
};
|
||||
|
||||
var $$promise$all$$default = function all(entries, label) {
|
||||
return new $$$enumerator$$default(this, entries, true /* abort on reject */, label).promise;
|
||||
};
|
||||
|
||||
var $$promise$race$$default = function race(entries, label) {
|
||||
/*jshint validthis:true */
|
||||
var Constructor = this;
|
||||
|
||||
var promise = new Constructor($$$internal$$noop, label);
|
||||
|
||||
if (!$$utils$$isArray(entries)) {
|
||||
$$$internal$$reject(promise, new TypeError('You must pass an array to race.'));
|
||||
return promise;
|
||||
}
|
||||
|
||||
var length = entries.length;
|
||||
|
||||
function onFulfillment(value) {
|
||||
$$$internal$$resolve(promise, value);
|
||||
}
|
||||
|
||||
function onRejection(reason) {
|
||||
$$$internal$$reject(promise, reason);
|
||||
}
|
||||
|
||||
for (var i = 0; promise._state === $$$internal$$PENDING && i < length; i++) {
|
||||
$$$internal$$subscribe(Constructor.resolve(entries[i]), undefined, onFulfillment, onRejection);
|
||||
}
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
var $$promise$resolve$$default = function resolve(object, label) {
|
||||
/*jshint validthis:true */
|
||||
var Constructor = this;
|
||||
|
||||
if (object && typeof object === 'object' && object.constructor === Constructor) {
|
||||
return object;
|
||||
}
|
||||
|
||||
var promise = new Constructor($$$internal$$noop, label);
|
||||
$$$internal$$resolve(promise, object);
|
||||
return promise;
|
||||
};
|
||||
|
||||
var $$promise$reject$$default = function reject(reason, label) {
|
||||
/*jshint validthis:true */
|
||||
var Constructor = this;
|
||||
var promise = new Constructor($$$internal$$noop, label);
|
||||
$$$internal$$reject(promise, reason);
|
||||
return promise;
|
||||
};
|
||||
|
||||
var $$es6$promise$promise$$counter = 0;
|
||||
|
||||
function $$es6$promise$promise$$needsResolver() {
|
||||
throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
|
||||
}
|
||||
|
||||
function $$es6$promise$promise$$needsNew() {
|
||||
throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
|
||||
}
|
||||
|
||||
var $$es6$promise$promise$$default = $$es6$promise$promise$$Promise;
|
||||
|
||||
/**
|
||||
Promise objects represent the eventual result of an asynchronous operation. The
|
||||
primary way of interacting with a promise is through its `then` method, which
|
||||
registers callbacks to receive either a promise’s eventual value or the reason
|
||||
why the promise cannot be fulfilled.
|
||||
|
||||
Terminology
|
||||
-----------
|
||||
|
||||
- `promise` is an object or function with a `then` method whose behavior conforms to this specification.
|
||||
- `thenable` is an object or function that defines a `then` method.
|
||||
- `value` is any legal JavaScript value (including undefined, a thenable, or a promise).
|
||||
- `exception` is a value that is thrown using the throw statement.
|
||||
- `reason` is a value that indicates why a promise was rejected.
|
||||
- `settled` the final resting state of a promise, fulfilled or rejected.
|
||||
|
||||
A promise can be in one of three states: pending, fulfilled, or rejected.
|
||||
|
||||
Promises that are fulfilled have a fulfillment value and are in the fulfilled
|
||||
state. Promises that are rejected have a rejection reason and are in the
|
||||
rejected state. A fulfillment value is never a thenable.
|
||||
|
||||
Promises can also be said to *resolve* a value. If this value is also a
|
||||
promise, then the original promise's settled state will match the value's
|
||||
settled state. So a promise that *resolves* a promise that rejects will
|
||||
itself reject, and a promise that *resolves* a promise that fulfills will
|
||||
itself fulfill.
|
||||
|
||||
|
||||
Basic Usage:
|
||||
------------
|
||||
|
||||
```js
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
// on success
|
||||
resolve(value);
|
||||
|
||||
// on failure
|
||||
reject(reason);
|
||||
});
|
||||
|
||||
promise.then(function(value) {
|
||||
// on fulfillment
|
||||
}, function(reason) {
|
||||
// on rejection
|
||||
});
|
||||
```
|
||||
|
||||
Advanced Usage:
|
||||
---------------
|
||||
|
||||
Promises shine when abstracting away asynchronous interactions such as
|
||||
`XMLHttpRequest`s.
|
||||
|
||||
```js
|
||||
function getJSON(url) {
|
||||
return new Promise(function(resolve, reject){
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('GET', url);
|
||||
xhr.onreadystatechange = handler;
|
||||
xhr.responseType = 'json';
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.send();
|
||||
|
||||
function handler() {
|
||||
if (this.readyState === this.DONE) {
|
||||
if (this.status === 200) {
|
||||
resolve(this.response);
|
||||
} else {
|
||||
reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getJSON('/posts.json').then(function(json) {
|
||||
// on fulfillment
|
||||
}, function(reason) {
|
||||
// on rejection
|
||||
});
|
||||
```
|
||||
|
||||
Unlike callbacks, promises are great composable primitives.
|
||||
|
||||
```js
|
||||
Promise.all([
|
||||
getJSON('/posts'),
|
||||
getJSON('/comments')
|
||||
]).then(function(values){
|
||||
values[0] // => postsJSON
|
||||
values[1] // => commentsJSON
|
||||
|
||||
return values;
|
||||
});
|
||||
```
|
||||
|
||||
@class Promise
|
||||
@param {function} resolver
|
||||
Useful for tooling.
|
||||
@constructor
|
||||
*/
|
||||
function $$es6$promise$promise$$Promise(resolver) {
|
||||
this._id = $$es6$promise$promise$$counter++;
|
||||
this._state = undefined;
|
||||
this._result = undefined;
|
||||
this._subscribers = [];
|
||||
|
||||
if ($$$internal$$noop !== resolver) {
|
||||
if (!$$utils$$isFunction(resolver)) {
|
||||
$$es6$promise$promise$$needsResolver();
|
||||
}
|
||||
|
||||
if (!(this instanceof $$es6$promise$promise$$Promise)) {
|
||||
$$es6$promise$promise$$needsNew();
|
||||
}
|
||||
|
||||
$$$internal$$initializePromise(this, resolver);
|
||||
}
|
||||
}
|
||||
|
||||
$$es6$promise$promise$$Promise.all = $$promise$all$$default;
|
||||
$$es6$promise$promise$$Promise.race = $$promise$race$$default;
|
||||
$$es6$promise$promise$$Promise.resolve = $$promise$resolve$$default;
|
||||
$$es6$promise$promise$$Promise.reject = $$promise$reject$$default;
|
||||
|
||||
$$es6$promise$promise$$Promise.prototype = {
|
||||
constructor: $$es6$promise$promise$$Promise,
|
||||
|
||||
/**
|
||||
The primary way of interacting with a promise is through its `then` method,
|
||||
which registers callbacks to receive either a promise's eventual value or the
|
||||
reason why the promise cannot be fulfilled.
|
||||
|
||||
```js
|
||||
findUser().then(function(user){
|
||||
// user is available
|
||||
}, function(reason){
|
||||
// user is unavailable, and you are given the reason why
|
||||
});
|
||||
```
|
||||
|
||||
Chaining
|
||||
--------
|
||||
|
||||
The return value of `then` is itself a promise. This second, 'downstream'
|
||||
promise is resolved with the return value of the first promise's fulfillment
|
||||
or rejection handler, or rejected if the handler throws an exception.
|
||||
|
||||
```js
|
||||
findUser().then(function (user) {
|
||||
return user.name;
|
||||
}, function (reason) {
|
||||
return 'default name';
|
||||
}).then(function (userName) {
|
||||
// If `findUser` fulfilled, `userName` will be the user's name, otherwise it
|
||||
// will be `'default name'`
|
||||
});
|
||||
|
||||
findUser().then(function (user) {
|
||||
throw new Error('Found user, but still unhappy');
|
||||
}, function (reason) {
|
||||
throw new Error('`findUser` rejected and we're unhappy');
|
||||
}).then(function (value) {
|
||||
// never reached
|
||||
}, function (reason) {
|
||||
// if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'.
|
||||
// If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'.
|
||||
});
|
||||
```
|
||||
If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream.
|
||||
|
||||
```js
|
||||
findUser().then(function (user) {
|
||||
throw new PedagogicalException('Upstream error');
|
||||
}).then(function (value) {
|
||||
// never reached
|
||||
}).then(function (value) {
|
||||
// never reached
|
||||
}, function (reason) {
|
||||
// The `PedgagocialException` is propagated all the way down to here
|
||||
});
|
||||
```
|
||||
|
||||
Assimilation
|
||||
------------
|
||||
|
||||
Sometimes the value you want to propagate to a downstream promise can only be
|
||||
retrieved asynchronously. This can be achieved by returning a promise in the
|
||||
fulfillment or rejection handler. The downstream promise will then be pending
|
||||
until the returned promise is settled. This is called *assimilation*.
|
||||
|
||||
```js
|
||||
findUser().then(function (user) {
|
||||
return findCommentsByAuthor(user);
|
||||
}).then(function (comments) {
|
||||
// The user's comments are now available
|
||||
});
|
||||
```
|
||||
|
||||
If the assimliated promise rejects, then the downstream promise will also reject.
|
||||
|
||||
```js
|
||||
findUser().then(function (user) {
|
||||
return findCommentsByAuthor(user);
|
||||
}).then(function (comments) {
|
||||
// If `findCommentsByAuthor` fulfills, we'll have the value here
|
||||
}, function (reason) {
|
||||
// If `findCommentsByAuthor` rejects, we'll have the reason here
|
||||
});
|
||||
```
|
||||
|
||||
Simple Example
|
||||
--------------
|
||||
|
||||
Synchronous Example
|
||||
|
||||
```javascript
|
||||
var result;
|
||||
|
||||
try {
|
||||
result = findResult();
|
||||
// success
|
||||
} catch(reason) {
|
||||
// failure
|
||||
}
|
||||
```
|
||||
|
||||
Errback Example
|
||||
|
||||
```js
|
||||
findResult(function(result, err){
|
||||
if (err) {
|
||||
// failure
|
||||
} else {
|
||||
// success
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Promise Example;
|
||||
|
||||
```javascript
|
||||
findResult().then(function(result){
|
||||
// success
|
||||
}, function(reason){
|
||||
// failure
|
||||
});
|
||||
```
|
||||
|
||||
Advanced Example
|
||||
--------------
|
||||
|
||||
Synchronous Example
|
||||
|
||||
```javascript
|
||||
var author, books;
|
||||
|
||||
try {
|
||||
author = findAuthor();
|
||||
books = findBooksByAuthor(author);
|
||||
// success
|
||||
} catch(reason) {
|
||||
// failure
|
||||
}
|
||||
```
|
||||
|
||||
Errback Example
|
||||
|
||||
```js
|
||||
|
||||
function foundBooks(books) {
|
||||
|
||||
}
|
||||
|
||||
function failure(reason) {
|
||||
|
||||
}
|
||||
|
||||
findAuthor(function(author, err){
|
||||
if (err) {
|
||||
failure(err);
|
||||
// failure
|
||||
} else {
|
||||
try {
|
||||
findBoooksByAuthor(author, function(books, err) {
|
||||
if (err) {
|
||||
failure(err);
|
||||
} else {
|
||||
try {
|
||||
foundBooks(books);
|
||||
} catch(reason) {
|
||||
failure(reason);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch(error) {
|
||||
failure(err);
|
||||
}
|
||||
// success
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Promise Example;
|
||||
|
||||
```javascript
|
||||
findAuthor().
|
||||
then(findBooksByAuthor).
|
||||
then(function(books){
|
||||
// found books
|
||||
}).catch(function(reason){
|
||||
// something went wrong
|
||||
});
|
||||
```
|
||||
|
||||
@method then
|
||||
@param {Function} onFulfilled
|
||||
@param {Function} onRejected
|
||||
Useful for tooling.
|
||||
@return {Promise}
|
||||
*/
|
||||
then: function(onFulfillment, onRejection) {
|
||||
var parent = this;
|
||||
var state = parent._state;
|
||||
|
||||
if (state === $$$internal$$FULFILLED && !onFulfillment || state === $$$internal$$REJECTED && !onRejection) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var child = new this.constructor($$$internal$$noop);
|
||||
var result = parent._result;
|
||||
|
||||
if (state) {
|
||||
var callback = arguments[state - 1];
|
||||
$$asap$$default(function(){
|
||||
$$$internal$$invokeCallback(state, child, callback, result);
|
||||
});
|
||||
} else {
|
||||
$$$internal$$subscribe(parent, child, onFulfillment, onRejection);
|
||||
}
|
||||
|
||||
return child;
|
||||
},
|
||||
|
||||
/**
|
||||
`catch` is simply sugar for `then(undefined, onRejection)` which makes it the same
|
||||
as the catch block of a try/catch statement.
|
||||
|
||||
```js
|
||||
function findAuthor(){
|
||||
throw new Error('couldn't find that author');
|
||||
}
|
||||
|
||||
// synchronous
|
||||
try {
|
||||
findAuthor();
|
||||
} catch(reason) {
|
||||
// something went wrong
|
||||
}
|
||||
|
||||
// async with promises
|
||||
findAuthor().catch(function(reason){
|
||||
// something went wrong
|
||||
});
|
||||
```
|
||||
|
||||
@method catch
|
||||
@param {Function} onRejection
|
||||
Useful for tooling.
|
||||
@return {Promise}
|
||||
*/
|
||||
'catch': function(onRejection) {
|
||||
return this.then(null, onRejection);
|
||||
}
|
||||
};
|
||||
|
||||
var $$es6$promise$polyfill$$default = function polyfill() {
|
||||
var local;
|
||||
|
||||
if (typeof global !== 'undefined') {
|
||||
local = global;
|
||||
} else if (typeof window !== 'undefined' && window.document) {
|
||||
local = window;
|
||||
} else {
|
||||
local = self;
|
||||
}
|
||||
|
||||
var es6PromiseSupport =
|
||||
"Promise" in local &&
|
||||
// Some of these methods are missing from
|
||||
// Firefox/Chrome experimental implementations
|
||||
"resolve" in local.Promise &&
|
||||
"reject" in local.Promise &&
|
||||
"all" in local.Promise &&
|
||||
"race" in local.Promise &&
|
||||
// Older version of the spec had a resolver object
|
||||
// as the arg rather than a function
|
||||
(function() {
|
||||
var resolve;
|
||||
new local.Promise(function(r) { resolve = r; });
|
||||
return $$utils$$isFunction(resolve);
|
||||
}());
|
||||
|
||||
if (!es6PromiseSupport) {
|
||||
local.Promise = $$es6$promise$promise$$default;
|
||||
}
|
||||
};
|
||||
|
||||
var es6$promise$umd$$ES6Promise = {
|
||||
'Promise': $$es6$promise$promise$$default,
|
||||
'polyfill': $$es6$promise$polyfill$$default
|
||||
};
|
||||
|
||||
/* global define:true module:true window: true */
|
||||
if (typeof define === 'function' && define['amd']) {
|
||||
define(function() { return es6$promise$umd$$ES6Promise; });
|
||||
} else if (typeof module !== 'undefined' && module['exports']) {
|
||||
module['exports'] = es6$promise$umd$$ES6Promise;
|
||||
} else if (typeof this !== 'undefined') {
|
||||
this['ES6Promise'] = es6$promise$umd$$ES6Promise;
|
||||
}
|
||||
}).call(this);
|
||||
@@ -0,0 +1,24 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>example arcade game</title>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" style="width: 50%; height: 50%"></canvas>
|
||||
<script type="text/javascript" src="es6-promise.js"></script>
|
||||
<script type="text/javascript" src="browserfs.js"></script>
|
||||
<script type="text/javascript" src="loader.js"></script>
|
||||
<script type="text/javascript">
|
||||
var emulator = new Emulator(document.querySelector("#canvas"),
|
||||
null,
|
||||
new JSMAMELoader(JSMAMELoader.driver("1943"),
|
||||
JSMAMELoader.nativeResolution(224, 256),
|
||||
JSMAMELoader.emulatorJS("emulators/mess1943.js"),
|
||||
JSMAMELoader.mountFile("1943.zip",
|
||||
JSMAMELoader.fetchFile("Game File",
|
||||
"examples/1943.zip"))))
|
||||
emulator.setScale(3);
|
||||
emulator.start({ waitAfterDownloading: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>example console game</title>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" style="width: 50%; height: 50%"/>
|
||||
<script type="text/javascript" src="es6-promise.js"></script>
|
||||
<script type="text/javascript" src="browserfs.js"></script>
|
||||
<script type="text/javascript" src="loader.js"></script>
|
||||
<script type="text/javascript">
|
||||
var emulator = new Emulator(document.querySelector("#canvas"),
|
||||
null,
|
||||
new JSMESSLoader(JSMESSLoader.driver("a2600"),
|
||||
JSMESSLoader.nativeResolution(352, 223),
|
||||
JSMESSLoader.emulatorJS("emulators/messa2600.js"),
|
||||
JSMESSLoader.mountFile("Pitfall_Activision_1982.bin",
|
||||
JSMESSLoader.fetchFile("Game File",
|
||||
"examples/Pitfall_Activision_1982.bin")),
|
||||
JSMESSLoader.mountFile("a2600.cfg",
|
||||
JSMESSLoader.fetchFile("Config File",
|
||||
"examples/a2600.cfg")),
|
||||
JSMESSLoader.peripheral("cart", "Pitfall_Activision_1982.bin")))
|
||||
emulator.setScale(3).start({ waitAfterDownloading: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>example dos game</title>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" style="width: 50%; height: 50%"/>
|
||||
<script type="text/javascript" src="es6-promise.js"></script>
|
||||
<script type="text/javascript" src="browserfs.js"></script>
|
||||
<script type="text/javascript" src="loader.js"></script>
|
||||
<script type="text/javascript">
|
||||
var emulator = new Emulator(document.querySelector("#canvas"),
|
||||
null,
|
||||
new DosBoxLoader(DosBoxLoader.emulatorJS("emulators/dosbox.js"),
|
||||
DosBoxLoader.nativeResolution(640, 400),
|
||||
DosBoxLoader.mountZip("c",
|
||||
DosBoxLoader.fetchFile("Game File",
|
||||
"examples/Zzt_1991_Epic_Megagames_Inc.zip")),
|
||||
DosBoxLoader.startExe("zzt.exe")))
|
||||
emulator.start({ waitAfterDownloading: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,170 +0,0 @@
|
||||
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 (fullscreenbutton) {
|
||||
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 += "<br />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);
|
||||
})();
|
||||
@@ -1,404 +0,0 @@
|
||||
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 splash_inverse = getComputedStyle(document.getElementsByTagName("body")[0]).backgroundColor === 'rgb(0, 0, 0)';
|
||||
|
||||
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();
|
||||
}());
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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 = splash_inverse ? 'white' : '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 = splash_inverse ? 'white' : '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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[19:14] <db48x> if you've got a list then I'll be happy to run through it
|
||||
[19:15] <SketchCow> Yeah, I think that's smart going forward.
|
||||
<SketchCow> https://archive.org/details/arcade_astrob
|
||||
[19:16] <SketchCow> Tests: Works with arcade machine, presentation
|
||||
[19:17] <SketchCow> https://archive.org/details/sg_Wiz_n_Liz_1993_Psygnosis_US
|
||||
<SketchCow> Tests: Game Console (Sega Genesis), Very intense processing needs
|
||||
[19:18] <SketchCow> https://archive.org/details/a2_Castle_Smurfenstein_1981_Dead_Smurf_cr
|
||||
<SketchCow> Tests: Apple II performance (computer), sound
|
||||
[19:19] <SketchCow> https://archive.org/details/msdos_Wolfenstein_3D_1992
|
||||
<SketchCow> Tests: EM-DOSBOX Side, sound, etc.
|
||||
<db48x> also parrallel file loads in Smurfenstein
|
||||
<SketchCow> So, at the VERY LEAST
|
||||
<SketchCow> These all shouldwork
|
||||
<SketchCow> If something blows up, there's something wrong.
|
||||
[19:20] <SketchCow> That's a solid test set.
|
||||
<SketchCow> Obviously, Dragon's Lair is our go-to for "holy fuck, large ROM"
|
||||
[20:31] <db48x> I'm going to add snack attack to that list, since it's easy to tell when it's running too fast
|
||||
[01:51] <SketchCow> Yes!
|
||||
|
||||
https://archive.org/details/msdos_Snack_Attack_II_1982&external_js=1
|
||||
Reference in New Issue
Block a user