/* * '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. * * '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. * * 'exe_name': * Executable name which used for find right binary to load * * 'resize_window_callback': * Function that is called when resize/orientationchanges/focus events happened */ var CUSTOM_PARAMETERS = { archive_location_filter: function( path ) { return ("archive" + path + ""); }, engine_arguments: ["--verify-graphics-calls=false",], custom_heap_size: 67108864, full_screen_container: "#canvas-container", disable_context_menu: true, retry_time:1.0, retry_count:10, unsupported_webgl_callback: function() { var e = document.getElementById("webgl-not-supported"); e.style.display = "block"; }, resize_window_callback: function() { var is_iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; var buttonHeight = 0; var prevInnerWidth = -1; var prevInnerHeight = -1; // Hack for iOS when exit from Fullscreen mode if (is_iOS) { window.scrollTo(0, 0); } var app_container = document.getElementById('app-container'); var game_canvas = document.getElementById('canvas'); var innerWidth = window.innerWidth; var innerHeight = window.innerHeight - buttonHeight; if (prevInnerWidth == innerWidth && prevInnerHeight == innerHeight) { return; } prevInnerWidth = innerWidth; prevInnerHeight = innerHeight; var width = 1920; var height = 1080; var targetRatio = width / height; var actualRatio = innerWidth / innerHeight; //Stretch width = innerWidth; height = innerHeight; var dpi = 1; dpi = window.devicePixelRatio || 1; app_container.style.width = width + "px"; app_container.style.height = height + buttonHeight + "px"; game_canvas.width = Math.floor(width * dpi); game_canvas.height = Math.floor(height * dpi); } } // 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 TypeError("No method specified"); if (typeof method === 'responseType') throw TypeError("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 onretry = this.onretry; var xhr = new XMLHttpRequest(); xhr._loadedSize = 0; xhr.open(method, url, true); xhr.responseType = responseType; xhr.onprogress = function(event) { if (onprogress) onprogress(xhr, event, xhr._loadedSize); xhr._loadedSize = event.loaded; }; xhr.onerror = function(event) { if (currentAttempt == FileLoader.options.retryCount) { if (onerror) onerror(xhr, event); return; } if (onretry) onretry(xhr, event, xhr._loadedSize, currentAttempt); xhr._loadedSize = 0; currentAttempt += 1; setTimeout(obj.send.bind(obj), FileLoader.options.retryInterval); }; xhr.onload = function(event) { if (onload) onload(xhr, event); }; 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(loadedDelta) // onerror(error) // onload(response) // onretry(loadedSize, currentAttempt) load: function(url, responseType, onprogress, onerror, onload, onretry) { var request = FileLoader.request(url, "GET", responseType); request.onprogress = function(xhr, e, ls) { var delta = e.loaded - ls; onprogress(delta); }; request.onerror = function(xhr, e) { onerror("Error loading '" + url + "' (" + e + ")"); }; request.onload = function(xhr, e) { if (xhr.readyState === XMLHttpRequest.DONE) { 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.onretry = function(xhr, event, loadedSize, currentAttempt) { onretry(loadedSize, currentAttempt); } request.send(); } }; var EngineLoader = { wasm_size: 2374239, wasmjs_size: 340441, asmjs_size: 4000000, wasm_instantiate_progress: 0, stream_wasm: "false" === "true", updateWasmInstantiateProgress: function(totalDownloadedSize) { EngineLoader.wasm_instantiate_progress = totalDownloadedSize * 0.1; }, // load and instantiate .wasm file using XMLHttpRequest loadAndInstantiateWasmAsync: function(src, imports, successCallback) { FileLoader.load(src, "arraybuffer", function(delta) { ProgressUpdater.updateCurrent(delta); }, function(error) { throw error; }, function(wasm) { if (wasm.byteLength != EngineLoader.wasm_size) { throw "Invalid wasm size. Expected: " + EngineLoader.wasm_size + ", actual: " + wasm.byteLength; } 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; }); }, function(loadedDelta, currentAttempt){ ProgressUpdater.updateCurrent(-loadedDelta); }); }, // stream and instantiate .wasm file streamAndInstantiateWasmAsync: async function(src, imports, successCallback) { // https://stackoverflow.com/a/69179454 var fetchFn = fetch; if (typeof TransformStream === "function" && ReadableStream.prototype.pipeThrough) { async function fetchWithProgress(path) { const response = await fetch(path); if (response.ok) { const ts = new TransformStream({ transform (chunk, controller) { ProgressUpdater.updateCurrent(chunk.byteLength); controller.enqueue(chunk); } }); return new Response(response.body.pipeThrough(ts), response); } else { return new Response(null, response); } } fetchFn = fetchWithProgress; } WebAssembly.instantiateStreaming(fetchFn(src), imports).then(function(output) { ProgressUpdater.updateCurrent(EngineLoader.wasm_instantiate_progress); successCallback(output.instance); }).catch(function(e) { console.log('wasm streaming instantiation failed! ' + e); console.log('Fallback to wasm loading'); EngineLoader.loadAndInstantiateWasmAsync(src, imports, successCallback); }); }, // instantiate the .wasm file either by streaming it or first loading and then instantiate it // https://github.com/emscripten-core/emscripten/blob/main/test/manual_wasm_instantiate.html loadWasmAsync: function(exeName) { Module.instantiateWasm = function(imports, successCallback) { if (EngineLoader.stream_wasm && (typeof WebAssembly.instantiateStreaming === "function")) { EngineLoader.streamAndInstantiateWasmAsync(exeName + ".wasm", imports, successCallback); } else { EngineLoader.loadAndInstantiateWasmAsync(exeName + ".wasm", imports, successCallback); } return {}; // Compiling asynchronously, no exports. }; EngineLoader.loadAndRunScriptAsync(exeName + '_wasm.js'); }, loadAsmJsAsync: function(exeName) { EngineLoader.loadAndRunScriptAsync(exeName + '_asmjs.js'); }, // load and start engine script (asm.js or wasm.js) loadAndRunScriptAsync: function(src) { FileLoader.load(src, "text", function(delta) { ProgressUpdater.updateCurrent(delta); }, function(error) { throw error; }, function(response) { var tag = document.createElement("script"); tag.text = response; document.body.appendChild(tag); }, function(loadedDelta, currentAttempt){ ProgressUpdater.updateCurrent(-loadedDelta); }); }, // left as entrypoint for backward capability // start loading archive_files.json // after receiving it - start loading engine and data concurrently load: function(appCanvasId, exeName) { ProgressView.addProgress(Module.setupCanvas(appCanvasId)); CUSTOM_PARAMETERS['exe_name'] = exeName; FileLoader.options.retryCount = CUSTOM_PARAMETERS["retry_count"]; FileLoader.options.retryInterval = CUSTOM_PARAMETERS["retry_time"] * 1000; if (typeof CUSTOM_PARAMETERS["can_not_download_file_callback"] === "function") { GameArchiveLoader.addFileDownloadErrorListener(CUSTOM_PARAMETERS["can_not_download_file_callback"]); } // Load and assemble archive GameArchiveLoader.addFileLoadedListener(Module.onArchiveFileLoaded); GameArchiveLoader.addArchiveLoadedListener(Module.onArchiveLoaded); GameArchiveLoader.setFileLocationFilter(CUSTOM_PARAMETERS["archive_location_filter"]); GameArchiveLoader.loadArchiveDescription('/archive_files.json'); // move resize callback setup here to make possible to override callback // from outside of dmloader.js if (typeof CUSTOM_PARAMETERS["resize_window_callback"] === "function") { var callback = CUSTOM_PARAMETERS["resize_window_callback"] callback(); window.addEventListener('resize', callback, false); window.addEventListener('orientationchange', callback, false); window.addEventListener('focus', callback, false); } } } /* ********************************************************************* */ /* 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 _archiveLocationFilter: function(path) { return "split" + path; }, cleanUp: function() { this._files = []; this._fileIndex = 0; this.isCompleted = false; this._onGameArchiveLoaderCompletedListeners = []; this._onAllTargetsBuiltListeners = []; this._onFileDownloadErrorListeners = []; }, addListener: function(list, callback) { if (typeof callback !== 'function') throw TypeError("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 RangeError("Buffer underflow. Start: " + start); } if (end > file.data.length) { throw RangeError("Buffer overflow. End : " + end + ", data length: " + file.data.length); } 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 pieces = file.pieces; for (i=0; i start) { throw RangeError("Segment underflow in file: " + file.name + ", offset: " + (previous.offset + previous.dataLength) + " , start: " + start); } } if (pieces.length - 2 > i) { var next = pieces[i + 1]; if (end > next.offset) { throw RangeError("Segment overflow in file: " + file.name + ", offset: " + next.offset + ", end: " + end); } } } } }, 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 ProgressView = { progress_id: "defold-progress", bar_id: "defold-progress-bar", addProgress : function (canvas) { /* Insert default progress bar below canvas */ canvas.insertAdjacentHTML('afterend', '
'); ProgressView.bar = document.getElementById(ProgressView.bar_id); ProgressView.progress = document.getElementById(ProgressView.progress_id); }, updateProgress: function(percentage) { if (ProgressView.bar) { ProgressView.bar.style.transform = "scaleX(" + Math.min(percentage, 100) / 100 + ")"; } }, removeProgress: function () { if (ProgressView.progress.parentElement !== null) { ProgressView.progress.parentElement.removeChild(ProgressView.progress); // Remove any background/splash image that was set in runApp(). // Workaround for Safari bug DEF-3061. Module.canvas.style.background = ""; } } }; var ProgressUpdater = { current: 0, total: 1, listeners: [], addListener: function(callback) { if (typeof callback !== 'function') throw TypeError("Invalid callback registration"); this.listeners.push(callback); }, notifyListeners: function(percentage) { for (i=0; i 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() { if (Module.persistentStorage != true) { return; } // 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(); try { FS.mkdir(dir); } catch (error) { Module.persistentStorage = false; Module._preloadAndCallMain(); return; } // 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. try { 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; } } catch (error) { Module.persistentStorage = false; Module._preloadAndCallMain(); return; } // Sync IDB->MEM before calling main() Module.preSync(function() { Module._preloadAndCallMain(); }); }], preRun: [function() { /* If archive is loaded, preload all its files */ if(Module._archiveLoaded) { Module.preloadAll(); } }], postRun: [function() { if(Module._archiveLoaded) { ProgressView.removeProgress(); } }], _preloadAndCallMain: function() { if (Module._syncInitial || Module.persistentStorage != true) { // If the archive isn't loaded, // we will have to wait with calling main. if (Module._archiveLoaded) { Module.preloadAll(); if (Module._isEngineLoaded) { // "Starting...." ProgressUpdater.complete(); Module._callMain(); } } } }, _callMain: function() { ProgressView.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.warn("Unable to synchronize mounted file systems: " + err); Module._syncTries += 1; } if (Module._syncNeeded) { Module._syncNeeded = false; Module._startSyncFS(); } }); } }, }; // common engine setup Module['persistentStorage'] = (typeof window !== 'undefined') && !!(window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB); Module['INITIAL_MEMORY'] = CUSTOM_PARAMETERS.custom_heap_size; Module['onRuntimeInitialized'] = function() { Module.runApp("canvas"); }; Module["locateFile"] = function(path, scriptDirectory) { // dmengine*.wasm is hardcoded in the built JS loader for WASM, // we need to replace it here with the correct project name. if (path == "dmengine.wasm" || path == "dmengine_release.wasm" || path == "dmengine_headless.wasm") { path = "Druid.wasm"; } return scriptDirectory + path; }; 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); }; };