return Module; }, }; (function() { var engine = Engine; var DOWNLOAD_ATTEMPTS_MAX = 4; var basePath = null; var engineLoadPromise = null; var loadingFiles = {}; function getBasePath(path) { if (path.endsWith('/')) path = path.slice(0, -1); if (path.lastIndexOf('.') > path.lastIndexOf('/')) path = path.slice(0, path.lastIndexOf('.')); return path; } function getBaseName(path) { path = getBasePath(path); return path.slice(path.lastIndexOf('/') + 1); } Engine = function Engine() { this.rtenv = null; var gameInitPromise = null; var unloadAfterInit = true; var progressFunc = null; var pckProgressTracker = {}; var lastProgress = { loaded: 0, total: 0 }; var canvas = null; var stdout = null; var stderr = null; this.initGame = function(mainPack) { if (!gameInitPromise) { if (mainPack === undefined) { if (basePath !== null) { mainPack = basePath + '.pck'; } else { return Promise.reject(new Error("No main pack to load specified")); } } if (basePath === null) basePath = getBasePath(mainPack); gameInitPromise = Engine.initEngine().then( instantiate.bind(this) ); var gameLoadPromise = loadPromise(mainPack, pckProgressTracker).then(function(xhr) { return xhr.response; }); gameInitPromise = Promise.all([gameLoadPromise, gameInitPromise]).then(function(values) { // resolve with pck return new Uint8Array(values[0]); }); if (unloadAfterInit) gameInitPromise.then(Engine.unloadEngine); requestAnimationFrame(animateProgress); } return gameInitPromise; }; function instantiate(initializer) { var rtenvOpts = { noInitialRun: true, thisProgram: getBaseName(basePath), engine: this, }; if (typeof stdout === 'function') rtenvOpts.print = stdout; if (typeof stderr === 'function') rtenvOpts.printErr = stderr; if (typeof WebAssembly === 'object' && initializer instanceof ArrayBuffer) { rtenvOpts.instantiateWasm = function(imports, onSuccess) { WebAssembly.instantiate(initializer, imports).then(function(result) { onSuccess(result.instance); }); return {}; }; } else { throw new Error("Invalid initializer"); } return new Promise(function(resolve, reject) { rtenvOpts.onRuntimeInitialized = resolve; rtenvOpts.onAbort = reject; rtenvOpts.engine.rtenv = Engine.RuntimeEnvironment(rtenvOpts); }); } this.start = function(mainPack) { return this.initGame(mainPack).then(synchronousStart.bind(this)); }; function synchronousStart(pckView) { // TODO don't expect canvas when runninng as cli tool if (canvas instanceof HTMLCanvasElement) { this.rtenv.canvas = canvas; } else { var firstCanvas = document.getElementsByTagName('canvas')[0]; if (firstCanvas instanceof HTMLCanvasElement) { this.rtenv.canvas = firstCanvas; } else { throw new Error("No canvas found"); } } var actualCanvas = this.rtenv.canvas; var context = false; try { context = actualCanvas.getContext('webgl2') || actualCanvas.getContext('experimental-webgl2'); } catch (e) {} if (!context) { throw new Error("WebGL 2 not available"); } // canvas can grab focus on click if (actualCanvas.tabIndex < 0) { actualCanvas.tabIndex = 0; } // necessary to calculate cursor coordinates correctly actualCanvas.style.padding = 0; actualCanvas.style.borderWidth = 0; actualCanvas.style.borderStyle = 'none'; // until context restoration is implemented actualCanvas.addEventListener('webglcontextlost', function(ev) { alert("WebGL context lost, please reload the page"); ev.preventDefault(); }, false); this.rtenv.FS.createDataFile('/', this.rtenv.thisProgram + '.pck', pckView, true, true, true); gameInitPromise = null; this.rtenv.callMain(); } this.setProgressFunc = function(func) { progressFunc = func; }; function animateProgress() { var loaded = 0; var total = 0; var totalIsValid = true; var progressIsFinal = true; [loadingFiles, pckProgressTracker].forEach(function(tracker) { Object.keys(tracker).forEach(function(file) { if (!tracker[file].final) progressIsFinal = false; if (!totalIsValid || tracker[file].total === 0) { totalIsValid = false; total = 0; } else { total += tracker[file].total; } loaded += tracker[file].loaded; }); }); if (loaded !== lastProgress.loaded || total !== lastProgress.total) { lastProgress.loaded = loaded; lastProgress.total = total; if (typeof progressFunc === 'function') progressFunc(loaded, total); } if (!progressIsFinal) requestAnimationFrame(animateProgress); } this.setCanvas = function(elem) { canvas = elem; }; this.setUnloadAfterInit = function(enabled) { if (enabled && !unloadAfterInit && gameInitPromise) { gameInitPromise.then(Engine.unloadEngine); } unloadAfterInit = enabled; }; this.setStdoutFunc = function(func) { var print = function(text) { if (arguments.length > 1) { text = Array.prototype.slice.call(arguments).join(" "); } func(text); }; if (this.rtenv) this.rtenv.print = print; stdout = print; }; this.setStderrFunc = function(func) { var printErr = function(text) { if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(" "); func(text); }; if (this.rtenv) this.rtenv.printErr = printErr; stderr = printErr; }; }; // Engine() Engine.RuntimeEnvironment = engine.RuntimeEnvironment; Engine.initEngine = function(newBasePath) { if (newBasePath !== undefined) basePath = getBasePath(newBasePath); if (engineLoadPromise === null) { if (typeof WebAssembly !== 'object') return Promise.reject(new Error("Browser doesn't support WebAssembly")); // TODO cache/retrieve module to/from idb engineLoadPromise = loadPromise(basePath + '.wasm').then(function(xhr) { return xhr.response; }); engineLoadPromise = engineLoadPromise.catch(function(err) { engineLoadPromise = null; throw err; }); } return engineLoadPromise; }; Engine.unloadEngine = function() { engineLoadPromise = null; }; function loadPromise(file, tracker) { if (tracker === undefined) tracker = loadingFiles; return new Promise(function(resolve, reject) { loadXHR(resolve, reject, file, tracker); }); } function loadXHR(resolve, reject, file, tracker) { var xhr = new XMLHttpRequest; xhr.open('GET', file); if (!file.endsWith('.js')) { xhr.responseType = 'arraybuffer'; } ['loadstart', 'progress', 'load', 'error', 'timeout', 'abort'].forEach(function(ev) { xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker)); }); xhr.send(); } function onXHREvent(resolve, reject, file, tracker, ev) { if (this.status >= 400) { if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { reject(new Error("Failed loading file '" + file + "': " + this.statusText)); this.abort(); return; } else { loadXHR(resolve, reject, file); } } switch (ev.type) { case 'loadstart': if (tracker[file] === undefined) { tracker[file] = { total: ev.total, loaded: ev.loaded, attempts: 0, final: false, }; } break; case 'progress': tracker[file].loaded = ev.loaded; tracker[file].total = ev.total; break; case 'load': tracker[file].final = true; resolve(this); break; case 'error': case 'timeout': if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { tracker[file].final = true; reject(new Error("Failed loading file '" + file + "'")); } else { loadXHR(resolve, reject, file); } break; case 'abort': tracker[file].final = true; reject(new Error("Loading file '" + file + "' was aborted.")); break; } } })();