// file downloader // wraps XMLHttpRequest and adds retry support and progress updates when the // content is gzipped (gzipped content doesn't report a computable content length // on Google Chrome) var FileLoader = { options: { retryCount: 4, retryInterval: 1000, }, // do xhr request with retries request: function(url, method, responseType, currentAttempt) { if (typeof method === 'undefined') throw "No method specified"; if (typeof method === 'responseType') throw "No responseType specified"; if (typeof currentAttempt === 'undefined') currentAttempt = 0; var obj = { send: function() { var onprogress = this.onprogress; var onload = this.onload; var onerror = this.onerror; var xhr = new XMLHttpRequest(); xhr.open(method, url, true); xhr.responseType = responseType; xhr.onprogress = function(e) { if (onprogress) onprogress(xhr, e); }; xhr.onerror = function(e) { if (currentAttempt == FileLoader.options.retryCount) { if (onerror) onerror(xhr, e); return; } currentAttempt = currentAttempt + 1; setTimeout(obj.send, FileLoader.options.retryInterval); }; xhr.onload = function(e) { if (onload) onload(xhr, e); }; xhr.send(null); } }; return obj; }, // Do HTTP HEAD request to get size of resource // callback will receive size or undefined in case of an error size: function(url, callback) { var request = FileLoader.request(url, "HEAD", "text"); request.onerror = function(xhr, e) { callback(undefined); }; request.onload = function(xhr, e) { if (xhr.readyState === 4) { if (xhr.status === 200) { var total = xhr.getResponseHeader('content-length'); callback(total); } else { callback(undefined); } } }; request.send(); }, // Do HTTP GET request // onprogress(loaded, total) // onerror(error) // onload(response) load: function(url, responseType, estimatedSize, onprogress, onerror, onload) { var request = FileLoader.request(url, "GET", responseType); request.onprogress = function(xhr, e) { if (e.lengthComputable) { onprogress(e.loaded, e.total); return; } var contentLength = xhr.getResponseHeader('content-length'); var size = contentLength != undefined ? contentLength : estimatedSize; if (size) { onprogress(e.loaded, size); } else { onprogress(e.loaded, e.loaded); } }; request.onerror = function(xhr, e) { onerror("Error loading '" + url + "' (" + e + ")"); }; request.onload = function(xhr, e) { if (xhr.readyState === 4) { if (xhr.status === 200) { var res = xhr.response; if (responseType == "json" && typeof res === "string") { onload(JSON.parse(res)); } else { onload(res); } } else { onerror("Error loading '" + url + "' (" + e + ")"); } } }; request.send(); } }; var EngineLoader = { wasm_size: 2000000, wasm_from: 0, wasm_to: 40, wasmjs_size: 250000, wasmjs_from: 40, wasmjs_to: 50, asmjs_size: 4000000, asmjs_from: 0, asmjs_to: 50, // load .wasm and set Module.instantiateWasm to use the loaded .wasm file // https://github.com/emscripten-core/emscripten/blob/master/tests/manual_wasm_instantiate.html#L170 loadWasmAsync: function(src, fromProgress, toProgress, callback) { FileLoader.load(src, "arraybuffer", EngineLoader.wasm_size, function(loaded, total) { Progress.calculateProgress(fromProgress, toProgress, loaded, total); }, function(error) { throw error; }, function(wasm) { Module.instantiateWasm = function(imports, successCallback) { var wasmInstantiate = WebAssembly.instantiate(new Uint8Array(wasm), imports).then(function(output) { successCallback(output.instance); }).catch(function(e) { console.log('wasm instantiation failed! ' + e); throw e; }); return {}; // Compiling asynchronously, no exports. } callback(); }); }, // load and start engine script (asm.js or wasm.js) loadScriptAsync: function(src, estimatedSize, fromProgress, toProgress) { FileLoader.load(src, "text", estimatedSize, function(loaded, total) { Progress.calculateProgress(fromProgress, toProgress, loaded, total); }, function(error) { throw error; }, function(response) { var tag = document.createElement("script"); tag.text = response; document.head.appendChild(tag); }); }, // load engine (asm.js or wasm.js + wasm) // engine load progress goes from 1-50% for ams.js // engine load progress goes from 0-40% for .wasm and 40-50% for wasm.js load: function(appCanvasId, exeName) { Progress.addProgress(Module.setupCanvas(appCanvasId)); if (Module['isWASMSupported']) { EngineLoader.loadWasmAsync(exeName + ".wasm", EngineLoader.wasm_from, EngineLoader.wasm_to, function(wasm) { EngineLoader.loadScriptAsync(exeName + '_wasm.js', EngineLoader.wasmjs_size, EngineLoader.wasmjs_from, EngineLoader.wasmjs_to); }); } else { EngineLoader.loadScriptAsync(exeName + '_asmjs.js', EngineLoader.asmjs_size, EngineLoader.asmjs_from, EngineLoader.asmjs_to); } } } /* ********************************************************************* */ /* Load and combine game archive data that is split into archives */ /* ********************************************************************* */ var GameArchiveLoader = { // which files to load _files: [], _fileIndex: 0, // file // name: intended filepath of built object // size: expected size of built object. // data: combined pieces // downloaded: total bytes downloaded // pieces: array of name, offset and data objects // numExpectedFiles: total number of files expected in description // lastRequestedPiece: index of last data file requested (strictly ascending) // totalLoadedPieces: counts the number pieces received //MAX_CONCURRENT_XHR: 6, // remove comment if throttling of XHR is desired. isCompleted: false, // status of process _onFileLoadedListeners: [], // signature: name, data. _onArchiveLoadedListeners:[], // signature: void _onFileDownloadErrorListeners: [], // signature: name _currentDownloadBytes: 0, _totalDownloadBytes: 0, _archiveLocationFilter: function(path) { return "split" + path; }, cleanUp: function() { this._files = []; this._fileIndex = 0; this.isCompleted = false; this._onGameArchiveLoaderCompletedListeners = []; this._onAllTargetsBuiltListeners = []; this._onFileDownloadErrorListeners = []; this._currentDownloadBytes = 0; this._totalDownloadBytes = 0; }, addListener: function(list, callback) { if (typeof callback !== 'function') throw "Invalid callback registration"; list.push(callback); }, notifyListeners: function(list, data) { for (i=0; i 1) { file.data = new Uint8Array(file.size); } // how many pieces to download at a time var limit = file.pieces.length; if (typeof this.MAX_CONCURRENT_XHR !== 'undefined') { limit = Math.min(limit, this.MAX_CONCURRENT_XHR); } // download pieces for (var i=0; i start) { throw "Buffer underflow"; } if (end > file.data.length) { throw "Buffer overflow"; } file.data.set(piece.data, piece.offset); } }, onPieceLoaded: function(file, piece) { this.addPieceToFile(file, piece); ++file.totalLoadedPieces; // is all pieces of the file loaded? if (file.totalLoadedPieces == file.pieces.length) { this.onFileLoaded(file); } // continue loading more pieces of the file // if not all pieces are already in progress else { var next = file.lastRequestedPiece + 1; if (next < file.pieces.length) { this.downloadPiece(file, next); } } }, verifyFile: function(file) { // verify that we downloaded as much as we were supposed to var actualSize = 0; for (var i=0;i 1) { var output = file.data; var pieces = file.pieces; for (i=0; i start) { throw "Segment underflow"; } } if (pieces.length - 2 > i) { var next = pieces[i + 1]; if (end > next.offset) { throw "Segment overflow"; } } } } }, onFileLoaded: function(file) { this.verifyFile(file); this.notifyFileLoaded(file); ++this._fileIndex; if (this._fileIndex == this._files.length) { this.onArchiveLoaded(); } else { this.downloadContent(); } }, onArchiveLoaded: function() { this.isCompleted = true; this.notifyArchiveLoaded(); } }; /* ********************************************************************* */ /* Default splash and progress visualisation */ /* ********************************************************************* */ var Progress = { progress_id: "defold-progress", bar_id: "defold-progress-bar", listeners: [], addListener: function(callback) { if (typeof callback !== 'function') throw "Invalid callback registration"; this.listeners.push(callback); }, notifyListeners: function(percentage) { for (i=0; i
'); Progress.bar = document.getElementById(Progress.bar_id); Progress.progress = document.getElementById(Progress.progress_id); amplitude.getInstance().logEvent('game: loading started'); }, updateProgress: function(percentage) { if (Progress.bar) { Progress.bar.style.width = percentage + "%"; } Progress.notifyListeners(percentage); }, calculateProgress: function (from, to, current, total) { this.updateProgress(from + (current / total) * (to - from)); }, removeProgress: function () { if (Progress.progress.parentElement !== null) { Progress.progress.parentElement.removeChild(Progress.progress); // Remove any background/splash image that was set in runApp(). // Workaround for Safari bug DEF-3061. Module.canvas.style.background = ""; amplitude.getInstance().logEvent('game: loading finished'); } } }; /* ********************************************************************* */ /* Default input override */ /* ********************************************************************* */ var CanvasInput = { arrowKeysHandler : function(e) { switch(e.keyCode) { case 37: case 38: case 39: case 40: // Arrow keys case 32: e.preventDefault(); e.stopPropagation(); // Space default: break; // do not block other keys } }, onFocusIn : function(e) { window.addEventListener("keydown", CanvasInput.arrowKeysHandler, false); }, onFocusOut: function(e) { window.removeEventListener("keydown", CanvasInput.arrowKeysHandler, false); }, addToCanvas : function(canvas) { canvas.addEventListener("focus", CanvasInput.onFocusIn, false); canvas.addEventListener("blur", CanvasInput.onFocusOut, false); canvas.focus(); CanvasInput.onFocusIn(); } }; /* ********************************************************************* */ /* Module is Emscripten namespace */ /* ********************************************************************* */ var Module = { noInitialRun: true, _filesToPreload: [], _archiveLoaded: false, _preLoadDone: false, _waitingForArchive: false, // Persistent storage persistentStorage: true, _syncInProgress: false, _syncNeeded: false, _syncInitial: false, _syncMaxTries: 3, _syncTries: 0, arguments: [], print: function(text) { console.log(text); }, printErr: function(text) { console.error(text); }, setStatus: function(text) { console.log(text); }, isWASMSupported: (function() { try { if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); if (module instanceof WebAssembly.Module) return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } catch (e) { } return false; })(), prepareErrorObject: function (err, url, line, column, errObj) { line = typeof line == "undefined" ? 0 : line; column = typeof column == "undefined" ? 0 : column; url = typeof url == "undefined" ? "" : url; var errorLine = url + ":" + line + ":" + column; var error = errObj || (typeof window.event != "undefined" ? window.event.error : "" ) || err || "Undefined Error"; var message = ""; var stack = ""; var backtrace = ""; if (typeof error == "object" && typeof error.stack != "undefined" && typeof error.message != "undefined") { stack = String(error.stack); message = String(error.message); } else { stack = String(error).split("\n"); message = stack.shift(); stack = stack.join("\n"); } stack = stack || errorLine; var callLine = /at (\S+:\d*$)/.exec(message); if (callLine) { message = message.replace(/(at \S+:\d*$)/, ""); stack = callLine[1] + "\n" + stack; } message = message.replace(/(abort\(.+\)) at .+/, "$1"); stack = stack.replace(/\?{1}\S+(:\d+:\d+)/g, "$1"); stack = stack.replace(/ *at (\S+)$/gm, "@$1"); stack = stack.replace(/ *at (\S+)(?: \[as \S+\])? +\((.+)\)/g, "$1@$2"); stack = stack.replace(/^((?:Object|Array)\.)/gm, ""); stack = stack.split("\n"); return { stack:stack, message:message }; }, hasWebGLSupport: function() { var webgl_support = false; try { var canvas = document.createElement("canvas"); var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); if (gl && gl instanceof WebGLRenderingContext) { webgl_support = true; } } catch (error) { console.log("An error occurred while detecting WebGL support: " + error); webgl_support = false; } return webgl_support; }, handleVisibilityChange: function () { GLFW.onFocusChanged(document[Module.hiddenProperty] ? 0 : 1); }, getHiddenProperty: function () { if ('hidden' in document) return 'hidden'; var prefixes = ['webkit','moz','ms','o']; for (var i = 0; i < prefixes.length; i++) { if ((prefixes[i] + 'Hidden') in document) return prefixes[i] + 'Hidden'; } return null; }, setupVisibilityChangeListener: function() { Module.hiddenProperty = Module.getHiddenProperty(); if( Module.hiddenProperty ) { var eventName = Module.hiddenProperty.replace(/[H|h]idden/,'') + 'visibilitychange'; document.addEventListener(eventName, Module.handleVisibilityChange, false); } else { console.log("No document.hidden property found. The focus events won't be enabled.") } }, setupCanvas: function(appCanvasId) { appCanvasId = (typeof appCanvasId === 'undefined') ? 'canvas' : appCanvasId; Module.canvas = document.getElementById(appCanvasId); return Module.canvas; }, /** * Module.runApp - Starts the application given a canvas element id * * 'extra_params' is an optional object that can have the following fields: * * 'archive_location_filter': * Filter function that will run for each archive path. * * 'unsupported_webgl_callback': * Function that is called if WebGL is not supported. * * 'engine_arguments': * List of arguments (strings) that will be passed to the engine. * * 'persistent_storage': * Boolean toggling the usage of persistent storage. * * 'custom_heap_size': * Number of bytes specifying the memory heap size. * * 'disable_context_menu': * Disables the right-click context menu on the canvas element if true. * * 'retry_time': * Pause before retry file loading after error. * * 'retry_count': * How many attempts we do when trying to download a file. * * 'can_not_download_file_callback': * Function that is called if you can't download file after 'retry_count' attempts. **/ runApp: function(appCanvasId, extra_params) { Module.setupCanvas(appCanvasId); var params = { archive_location_filter: function(path) { return 'split' + path; }, unsupported_webgl_callback: undefined, engine_arguments: [], persistent_storage: true, custom_heap_size: undefined, disable_context_menu: true, retry_time: 1, retry_count: 10, can_not_download_file_callback: undefined, }; for (var k in extra_params) { if (extra_params.hasOwnProperty(k)) { params[k] = extra_params[k]; } } Module.arguments = params["engine_arguments"]; Module.persistentStorage = params["persistent_storage"]; var fullScreenContainer = params["full_screen_container"]; if (typeof fullScreenContainer === "string") { fullScreenContainer = document.querySelector(fullScreenContainer); } Module.fullScreenContainer = fullScreenContainer || Module.canvas; if (Module.hasWebGLSupport()) { // Override game keys CanvasInput.addToCanvas(Module.canvas); Module.setupVisibilityChangeListener(); // Add context menu hide-handler if requested if (params["disable_context_menu"]) { Module.canvas.oncontextmenu = function(e) { e.preventDefault(); }; } FileLoader.options.retryCount = params["retry_count"]; FileLoader.options.retryInterval = params["retry_time"] * 1000; if (typeof params["can_not_download_file_callback"] === "function") { GameArchiveLoader.addFileDownloadErrorListener(params["can_not_download_file_callback"]); } // Load and assemble archive GameArchiveLoader.addFileLoadedListener(Module.onArchiveFileLoaded); GameArchiveLoader.addArchiveLoadedListener(Module.onArchiveLoaded); GameArchiveLoader.setFileLocationFilter(params["archive_location_filter"]); GameArchiveLoader.loadArchiveDescription('/archive_files.json'); } else { Progress.updateProgress(100, "Unable to start game, WebGL not supported"); Module.setStatus = function(text) { if (text) Module.printErr('[missing WebGL] ' + text); }; if (typeof params["unsupported_webgl_callback"] === "function") { params["unsupported_webgl_callback"](); } } }, onArchiveFileLoaded: function(file) { Module._filesToPreload.push({path: file.name, data: file.data}); }, onArchiveLoaded: function() { GameArchiveLoader.cleanUp(); Module._archiveLoaded = true; Progress.updateProgress(100, "Starting..."); if (Module._waitingForArchive) { Module._preloadAndCallMain(); } }, toggleFullscreen: function(element) { if (GLFW.isFullscreen) { GLFW.cancelFullScreen(); } else { GLFW.requestFullScreen(element); } }, preSync: function(done) { // Initial persistent sync before main is called FS.syncfs(true, function(err) { if(err) { Module._syncTries += 1; console.error("FS syncfs error: " + err); if (Module._syncMaxTries > Module._syncTries) { Module.preSync(done); } else { Module._syncInitial = true; done(); } } else { Module._syncInitial = true; if (done !== undefined) { done(); } } }); }, preloadAll: function() { if (Module._preLoadDone) { return; } Module._preLoadDone = true; for (var i = 0; i < Module._filesToPreload.length; ++i) { var item = Module._filesToPreload[i]; FS.createPreloadedFile("", item.path, item.data, true, true); } }, // Tries to do a MEM->IDB sync // It will flag that another one is needed if there is already one sync running. persistentSync: function() { // Need to wait for the initial sync to finish since it // will call close on all its file streams which will trigger // new persistentSync for each. if (Module._syncInitial) { if (Module._syncInProgress) { Module._syncNeeded = true; } else { Module._startSyncFS(); } } }, preInit: [function() { /* Mount filesystem on preinit */ var dir = DMSYS.GetUserPersistentDataRoot(); FS.mkdir(dir); // If IndexedDB is supported we mount the persistent data root as IDBFS, // then try to do a IDB->MEM sync before we start the engine to get // previously saved data before boot. window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; if (Module.persistentStorage && window.indexedDB) { FS.mount(IDBFS, {}, dir); // Patch FS.close so it will try to sync MEM->IDB var _close = FS.close; FS.close = function(stream) { var r = _close(stream); Module.persistentSync(); return r; } // Sync IDB->MEM before calling main() Module.preSync(function() { Module._preloadAndCallMain(); }); } else { Module._preloadAndCallMain(); } }], preRun: [function() { /* If archive is loaded, preload all its files */ if(Module._archiveLoaded) { Module.preloadAll(); } }], postRun: [function() { if(Module._archiveLoaded) { Progress.removeProgress(); } }], _preloadAndCallMain: function() { // If the archive isn't loaded, // we will have to wait with calling main. if (!Module._archiveLoaded) { Module._waitingForArchive = true; } else { Module.preloadAll(); Progress.removeProgress(); if (Module.callMain === undefined) { Module.noInitialRun = false; } else { Module.callMain(Module.arguments); } } }, // Wrap IDBFS syncfs call with logic to avoid multiple syncs // running at the same time. _startSyncFS: function() { Module._syncInProgress = true; if (Module._syncMaxTries > Module._syncTries) { FS.syncfs(false, function(err) { Module._syncInProgress = false; if (err) { console.error("Module._startSyncFS error: " + err); Module._syncTries += 1; } if (Module._syncNeeded) { Module._syncNeeded = false; Module._startSyncFS(); } }); } }, }; window.onerror = function(err, url, line, column, errObj) { if (typeof Module.ccall !== 'undefined') { var errorObject = Module.prepareErrorObject(err, url, line, column, errObj); Module.ccall('JSWriteDump', 'null', ['string'], [JSON.stringify(errorObject.stack)]); } Module.setStatus('Exception thrown, see JavaScript console'); Module.setStatus = function(text) { if (text) Module.printErr('[post-exception status] ' + text); }; }; window.addEventListener("beforeunload", function(event) { amplitude.getInstance().logEvent('game: page unloaded'); amplitude.getInstance().sendEvents(); }); setInterval(function() { if (typeof document.hidden === 'undefined' || !document.hidden) { amplitude.getInstance().logEvent('game: keep alive'); } }, 300 * 1000);