dmloader.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. /* ********************************************************************* */
  2. /* Load and combine data that is split into archives */
  3. /* ********************************************************************* */
  4. var Combine = {
  5. _targets: [],
  6. _targetIndex: 0,
  7. // target: build target
  8. // name: intended filepath of built object
  9. // size: expected size of built object.
  10. // data: combined data
  11. // downloaded: total amount of data downloaded
  12. // pieces: array of name, offset and data objects
  13. // numExpectedFiles: total number of files expected in description
  14. // lastRequestedPiece: index of last data file requested (strictly ascending)
  15. // totalLoadedPieces: counts the number of data files received
  16. //MAX_CONCURRENT_XHR: 6, // remove comment if throttling of XHR is desired.
  17. isCompleted: false, // status of process
  18. _onCombineCompleted: [], // signature: name, data.
  19. _onAllTargetsBuilt:[], // signature: void
  20. _onDownloadProgress: [], // signature: downloaded, total
  21. _totalDownloadBytes: 0,
  22. _archiveLocationFilter: function(path) { return "split" + path; },
  23. addProgressListener: function(callback) {
  24. if (typeof callback !== 'function') {
  25. throw "Invalid callback registration";
  26. }
  27. this._onDownloadProgress.push(callback);
  28. },
  29. addCombineCompletedListener: function(callback) {
  30. if (typeof callback !== 'function') {
  31. throw "Invalid callback registration";
  32. }
  33. this._onCombineCompleted.push(callback);
  34. },
  35. addAllTargetsBuiltListener: function(callback) {
  36. if (typeof callback !== 'function') {
  37. throw "Invalid callback registration";
  38. }
  39. this._onAllTargetsBuilt.push(callback);
  40. },
  41. // descriptUrl: location of text file describing files to be preloaded
  42. process: function(descriptUrl) {
  43. var xhr = new XMLHttpRequest();
  44. xhr.open('GET', descriptUrl);
  45. xhr.responseType = 'text';
  46. xhr.onload = function(evt) {
  47. Combine.onReceiveDescription(xhr);
  48. };
  49. xhr.send(null);
  50. },
  51. cleanUp: function() {
  52. this._targets = [];
  53. this._targetIndex = 0;
  54. this.isCompleted = false;
  55. this._onCombineCompleted = [];
  56. this._onAllTargetsBuilt = [];
  57. this._onDownloadProgress = [];
  58. this._totalDownloadBytes = 0;
  59. },
  60. onReceiveDescription: function(xhr) {
  61. var json = JSON.parse(xhr.responseText);
  62. this._targets = json.content;
  63. this._totalDownloadBytes = 0;
  64. var targets = this._targets;
  65. for(var i=0; i<targets.length; ++i) {
  66. this._totalDownloadBytes += targets[i].size;
  67. }
  68. this.requestContent();
  69. },
  70. requestContent: function() {
  71. var target = this._targets[this._targetIndex];
  72. if (1 < target.pieces.length) {
  73. target.data = new Uint8Array(target.size);
  74. }
  75. var limit = target.pieces.length;
  76. if (typeof this.MAX_CONCURRENT_XHR !== 'undefined') {
  77. limit = Math.min(limit, this.MAX_CONCURRENT_XHR);
  78. }
  79. for (var i=0; i<limit; ++i) {
  80. this.requestPiece(target, i);
  81. }
  82. },
  83. requestPiece: function(target, index) {
  84. if (index < target.lastRequestedPiece) {
  85. throw "Request out of order";
  86. }
  87. target.lastRequestedPiece = index;
  88. target.progress = {};
  89. var item = target.pieces[index];
  90. var xhr = new XMLHttpRequest();
  91. xhr.open('GET', this._archiveLocationFilter('/' + item.name), true);
  92. xhr.responseType = 'arraybuffer';
  93. xhr.onprogress = function(evt) {
  94. target.progress[item.name] = {total: 0, downloaded: 0};
  95. if (evt.total && evt.lengthComputable) {
  96. target.progress[item.name].total = evt.total;
  97. }
  98. if (evt.loaded && evt.lengthComputable) {
  99. target.progress[item.name].downloaded = evt.loaded;
  100. Combine.updateProgress(target);
  101. }
  102. };
  103. xhr.onload = function(evt) {
  104. item.data = new Uint8Array(xhr.response);
  105. item.dataLength = item.data.length;
  106. target.progress[item.name].total = item.dataLength;
  107. target.progress[item.name].downloaded = item.dataLength;
  108. Combine.copyData(target, item);
  109. Combine.onPieceLoaded(target, item);
  110. Combine.updateProgress(target);
  111. item.data = undefined;
  112. };
  113. xhr.send(null);
  114. },
  115. updateProgress: function(target) {
  116. var total_downloaded = 0;
  117. for (var p in target.progress) {
  118. total_downloaded += target.progress[p].downloaded;
  119. }
  120. for(i = 0; i<this._onDownloadProgress.length; ++i) {
  121. this._onDownloadProgress[i](total_downloaded, this._totalDownloadBytes);
  122. }
  123. },
  124. copyData: function(target, item) {
  125. if (1 == target.pieces.length) {
  126. target.data = item.data;
  127. } else {
  128. var start = item.offset;
  129. var end = start + item.data.length;
  130. if (0 > start) {
  131. throw "Buffer underflow";
  132. }
  133. if (end > target.data.length) {
  134. throw "Buffer overflow";
  135. }
  136. target.data.set(item.data, item.offset);
  137. }
  138. },
  139. onPieceLoaded: function(target, item) {
  140. if (typeof target.totalLoadedPieces === 'undefined') {
  141. target.totalLoadedPieces = 0;
  142. }
  143. ++target.totalLoadedPieces;
  144. if (target.totalLoadedPieces == target.pieces.length) {
  145. this.finalizeTarget(target);
  146. ++this._targetIndex;
  147. for (var i=0; i<this._onCombineCompleted.length; ++i) {
  148. this._onCombineCompleted[i](target.name, target.data);
  149. }
  150. if (this._targetIndex < this._targets.length) {
  151. this.requestContent();
  152. } else {
  153. this.isCompleted = true;
  154. for (i=0; i<this._onAllTargetsBuilt.length; ++i) {
  155. this._onAllTargetsBuilt[i]();
  156. }
  157. }
  158. } else {
  159. var next = target.lastRequestedPiece + 1;
  160. if (next < target.pieces.length) {
  161. this.requestPiece(target, next);
  162. }
  163. }
  164. },
  165. finalizeTarget: function(target) {
  166. var actualSize = 0;
  167. for (var i=0;i<target.pieces.length; ++i) {
  168. actualSize += target.pieces[i].dataLength;
  169. }
  170. if (actualSize != target.size) {
  171. throw "Unexpected data size";
  172. }
  173. if (1 < target.pieces.length) {
  174. var output = target.data;
  175. var pieces = target.pieces;
  176. for (i=0; i<pieces.length; ++i) {
  177. var item = pieces[i];
  178. // Bounds check
  179. var start = item.offset;
  180. var end = start + item.dataLength;
  181. if (0 < i) {
  182. var previous = pieces[i - 1];
  183. if (previous.offset + previous.dataLength > start) {
  184. throw "Segment underflow";
  185. }
  186. }
  187. if (pieces.length - 2 > i) {
  188. var next = pieces[i + 1];
  189. if (end > next.offset) {
  190. throw "Segment overflow";
  191. }
  192. }
  193. }
  194. }
  195. }
  196. };
  197. /* ********************************************************************* */
  198. /* Default splash and progress visualisation */
  199. /* ********************************************************************* */
  200. var Progress = {
  201. progress_id: "defold-progress",
  202. bar_id: "defold-progress-bar",
  203. addProgress : function (canvas) {
  204. /* Insert default progress bar below canvas */
  205. canvas.insertAdjacentHTML('afterend', '<div id="' + Progress.progress_id + '" class="canvas-app-progress"><div id="' + Progress.bar_id + '" class="canvas-app-progress-bar" style="width: 0%;">0%</div></div>');
  206. Progress.bar = document.getElementById(Progress.bar_id);
  207. Progress.progress = document.getElementById(Progress.progress_id);
  208. },
  209. updateProgress: function (percentage, text) {
  210. Progress.bar.style.width = percentage + "%";
  211. text = (typeof text === 'undefined') ? Math.round(percentage) + "%" : text;
  212. Progress.bar.innerText = text;
  213. },
  214. removeProgress: function () {
  215. if (Progress.progress.parentElement !== null) {
  216. Progress.progress.parentElement.removeChild(Progress.progress);
  217. // Remove any background/splash image that was set in runApp().
  218. // Workaround for Safari bug DEF-3061.
  219. Module.canvas.style.background = "";
  220. Module["on_game_start"]();
  221. }
  222. }
  223. };
  224. /* ********************************************************************* */
  225. /* Default input override */
  226. /* ********************************************************************* */
  227. var CanvasInput = {
  228. arrowKeysHandler : function(e) {
  229. switch(e.keyCode) {
  230. case 37: case 38: case 39: case 40: // Arrow keys
  231. case 32: e.preventDefault(); e.stopPropagation(); // Space
  232. default: break; // do not block other keys
  233. }
  234. },
  235. onFocusIn : function(e) {
  236. window.addEventListener("keydown", CanvasInput.arrowKeysHandler, false);
  237. },
  238. onFocusOut: function(e) {
  239. window.removeEventListener("keydown", CanvasInput.arrowKeysHandler, false);
  240. },
  241. addToCanvas : function(canvas) {
  242. canvas.addEventListener("focus", CanvasInput.onFocusIn, false);
  243. canvas.addEventListener("blur", CanvasInput.onFocusOut, false);
  244. canvas.focus();
  245. CanvasInput.onFocusIn();
  246. }
  247. };
  248. /* ********************************************************************* */
  249. /* Module is Emscripten namespace */
  250. /* ********************************************************************* */
  251. var Module = {
  252. noInitialRun: true,
  253. _filesToPreload: [],
  254. _archiveLoaded: false,
  255. _preLoadDone: false,
  256. _waitingForArchive: false,
  257. // Persistent storage
  258. persistentStorage: true,
  259. _syncInProgress: false,
  260. _syncNeeded: false,
  261. _syncInitial: false,
  262. _syncMaxTries: 3,
  263. _syncTries: 0,
  264. print: function(text) { console.log(text); },
  265. printErr: function(text) { console.error(text); },
  266. setStatus: function(text) { console.log(text); },
  267. isWASMSupported: (function() {
  268. try {
  269. if (typeof WebAssembly === "object"
  270. && typeof WebAssembly.instantiate === "function") {
  271. const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
  272. if (module instanceof WebAssembly.Module)
  273. return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
  274. }
  275. } catch (e) {
  276. }
  277. return false;
  278. })(),
  279. prepareErrorObject: function (err, url, line, column, errObj) {
  280. line = typeof line == "undefined" ? 0 : line;
  281. column = typeof column == "undefined" ? 0 : column;
  282. url = typeof url == "undefined" ? "" : url;
  283. var errorLine = url + ":" + line + ":" + column;
  284. var error = errObj || (typeof window.event != "undefined" ? window.event.error : "" ) || err || "Undefined Error";
  285. var message = "";
  286. var stack = "";
  287. var backtrace = "";
  288. if (typeof error == "object" && typeof error.stack != "undefined" && typeof error.message != "undefined") {
  289. stack = String(error.stack);
  290. message = String(error.message);
  291. } else {
  292. stack = String(error).split("\n");
  293. message = stack.shift();
  294. stack = stack.join("\n");
  295. }
  296. stack = stack || errorLine;
  297. var callLine = /at (\S+:\d*$)/.exec(message);
  298. if (callLine) {
  299. message = message.replace(/(at \S+:\d*$)/, "");
  300. stack = callLine[1] + "\n" + stack;
  301. }
  302. message = message.replace(/(abort\(.+\)) at .+/, "$1");
  303. stack = stack.replace(/\?{1}\S+(:\d+:\d+)/g, "$1");
  304. stack = stack.replace(/ *at (\S+)$/gm, "@$1");
  305. stack = stack.replace(/ *at (\S+)(?: \[as \S+\])? +\((.+)\)/g, "$1@$2");
  306. stack = stack.replace(/^((?:Object|Array)\.)/gm, "");
  307. stack = stack.split("\n");
  308. return { stack:stack, message:message };
  309. },
  310. hasWebGLSupport: function() {
  311. var webgl_support = false;
  312. try {
  313. var canvas = document.createElement("canvas");
  314. var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
  315. if (gl && gl instanceof WebGLRenderingContext) {
  316. webgl_support = true;
  317. }
  318. } catch (error) {
  319. console.log("An error occurred while detecting WebGL support: " + error);
  320. webgl_support = false;
  321. }
  322. return webgl_support;
  323. },
  324. handleVisibilityChange: function () {
  325. GLFW.onFocusChanged(document[Module.hiddenProperty] ? 0 : 1);
  326. },
  327. getHiddenProperty: function () {
  328. if ('hidden' in document) return 'hidden';
  329. var prefixes = ['webkit','moz','ms','o'];
  330. for (var i = 0; i < prefixes.length; i++){
  331. if ((prefixes[i] + 'Hidden') in document)
  332. return prefixes[i] + 'Hidden';
  333. }
  334. return null;
  335. },
  336. setupVisibilityChangeListener: function() {
  337. Module.hiddenProperty = Module.getHiddenProperty();
  338. if( Module.hiddenProperty ) {
  339. var eventName = Module.hiddenProperty.replace(/[H|h]idden/,'') + 'visibilitychange';
  340. document.addEventListener(eventName, Module.handleVisibilityChange, false);
  341. } else {
  342. console.log("No document.hidden property found. The focus events won't be enabled.")
  343. }
  344. },
  345. /**
  346. * Module.runApp - Starts the application given a canvas element id
  347. *
  348. * 'extra_params' is an optional object that can have the following fields:
  349. *
  350. * 'splash_image':
  351. * Path to an image that should be used as a background image for
  352. * the canvas element.
  353. *
  354. * 'archive_location_filter':
  355. * Filter function that will run for each archive path.
  356. *
  357. * 'unsupported_webgl_callback':
  358. * Function that is called if WebGL is not supported.
  359. *
  360. * 'engine_arguments':
  361. * List of arguments (strings) that will be passed to the engine.
  362. *
  363. * 'persistent_storage':
  364. * Boolean toggling the usage of persistent storage.
  365. *
  366. * 'custom_heap_size':
  367. * Number of bytes specifying the memory heap size.
  368. *
  369. * 'disable_context_menu':
  370. * Disables the right-click context menu on the canvas element if true.
  371. *
  372. **/
  373. runApp: function(app_canvas_id, extra_params) {
  374. app_canvas_id = (typeof app_canvas_id === 'undefined') ? 'canvas' : app_canvas_id;
  375. var params = {
  376. splash_image: undefined,
  377. archive_location_filter: function(path) { return 'split' + path; },
  378. unsupported_webgl_callback: undefined,
  379. engine_arguments: [],
  380. persistent_storage: true,
  381. custom_heap_size: undefined,
  382. disable_context_menu: true
  383. };
  384. for (var k in extra_params) {
  385. if (extra_params.hasOwnProperty(k)) {
  386. params[k] = extra_params[k];
  387. }
  388. }
  389. Module.canvas = document.getElementById(app_canvas_id);
  390. if (typeof params["splash_image"] !== 'undefined') {
  391. Module.canvas.style.background = 'no-repeat center url("' + params["splash_image"] + '")';
  392. }
  393. Module.arguments = params["engine_arguments"];
  394. Module.persistentStorage = params["persistent_storage"];
  395. Module["TOTAL_MEMORY"] = params["custom_heap_size"];
  396. Module["on_game_start"] = params["game_start"];
  397. if (Module.hasWebGLSupport()) {
  398. // Override game keys
  399. CanvasInput.addToCanvas(Module.canvas);
  400. Module.setupVisibilityChangeListener();
  401. // Add progress visuals
  402. Progress.addProgress(Module.canvas);
  403. // Add context menu hide-handler if requested
  404. if (params["disable_context_menu"])
  405. {
  406. Module.canvas.oncontextmenu = function(e) {
  407. e.preventDefault();
  408. };
  409. }
  410. // Load and assemble archive
  411. Combine.addCombineCompletedListener(Module.onArchiveFileLoaded);
  412. Combine.addAllTargetsBuiltListener(Module.onArchiveLoaded);
  413. Combine.addProgressListener(Module.onArchiveLoadProgress);
  414. Combine._archiveLocationFilter = params["archive_location_filter"];
  415. Combine.process(Combine._archiveLocationFilter('/archive_files.json'));
  416. } else {
  417. Progress.addProgress(Module.canvas);
  418. Progress.updateProgress(100, "Unable to start game, WebGL not supported");
  419. Module.setStatus = function(text) {
  420. if (text) Module.printErr('[missing WebGL] ' + text);
  421. };
  422. if (typeof params["unsupported_webgl_callback"] === "function") {
  423. params["unsupported_webgl_callback"]();
  424. }
  425. }
  426. },
  427. onArchiveLoadProgress: function(downloaded, total) {
  428. Progress.updateProgress(downloaded / total * 100);
  429. },
  430. onArchiveFileLoaded: function(name, data) {
  431. Module._filesToPreload.push({path: name, data: data});
  432. },
  433. onArchiveLoaded: function() {
  434. Combine.cleanUp();
  435. Module._archiveLoaded = true;
  436. Progress.updateProgress(100, "Starting...");
  437. if (Module._waitingForArchive) {
  438. Module._preloadAndCallMain();
  439. }
  440. },
  441. toggleFullscreen: function() {
  442. if (GLFW.isFullscreen) {
  443. GLFW.cancelFullScreen();
  444. } else {
  445. GLFW.requestFullScreen();
  446. }
  447. },
  448. preSync: function(done) {
  449. // Initial persistent sync before main is called
  450. FS.syncfs(true, function(err) {
  451. if(err) {
  452. Module._syncTries += 1;
  453. console.error("FS syncfs error: " + err);
  454. if (Module._syncMaxTries > Module._syncTries) {
  455. Module.preSync(done);
  456. } else {
  457. Module._syncInitial = true;
  458. done();
  459. }
  460. } else {
  461. Module._syncInitial = true;
  462. if (done !== undefined) {
  463. done();
  464. }
  465. }
  466. });
  467. },
  468. preloadAll: function() {
  469. if (Module._preLoadDone) {
  470. return;
  471. }
  472. Module._preLoadDone = true;
  473. for (var i = 0; i < Module._filesToPreload.length; ++i) {
  474. var item = Module._filesToPreload[i];
  475. FS.createPreloadedFile("", item.path, item.data, true, true);
  476. }
  477. },
  478. // Tries to do a MEM->IDB sync
  479. // It will flag that another one is needed if there is already one sync running.
  480. persistentSync: function() {
  481. // Need to wait for the initial sync to finish since it
  482. // will call close on all its file streams which will trigger
  483. // new persistentSync for each.
  484. if (Module._syncInitial) {
  485. if (Module._syncInProgress) {
  486. Module._syncNeeded = true;
  487. } else {
  488. Module._startSyncFS();
  489. }
  490. }
  491. },
  492. preInit: [function() {
  493. /* Mount filesystem on preinit */
  494. var dir = DMSYS.GetUserPersistentDataRoot();
  495. FS.mkdir(dir);
  496. // If IndexedDB is supported we mount the persistent data root as IDBFS,
  497. // then try to do a IDB->MEM sync before we start the engine to get
  498. // previously saved data before boot.
  499. window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
  500. if (Module.persistentStorage && window.indexedDB) {
  501. FS.mount(IDBFS, {}, dir);
  502. // Patch FS.close so it will try to sync MEM->IDB
  503. var _close = FS.close; FS.close = function(stream) { var r = _close(stream); Module.persistentSync(); return r; }
  504. // Sync IDB->MEM before calling main()
  505. Module.preSync(function() {
  506. Module._preloadAndCallMain();
  507. });
  508. } else {
  509. Module._preloadAndCallMain();
  510. }
  511. }],
  512. preRun: [function() {
  513. /* If archive is loaded, preload all its files */
  514. if(Module._archiveLoaded) {
  515. Module.preloadAll();
  516. }
  517. }],
  518. postRun: [function() {
  519. if(Module._archiveLoaded) {
  520. Progress.removeProgress();
  521. }
  522. }],
  523. _preloadAndCallMain: function() {
  524. // If the archive isn't loaded,
  525. // we will have to wait with calling main.
  526. if (!Module._archiveLoaded) {
  527. Module._waitingForArchive = true;
  528. } else {
  529. // Need to set heap size before calling main
  530. TOTAL_MEMORY = Module["TOTAL_MEMORY"] || TOTAL_MEMORY;
  531. Module.preloadAll();
  532. Progress.removeProgress();
  533. if (Module.callMain === undefined) {
  534. Module.noInitialRun = false;
  535. } else {
  536. Module.callMain(Module.arguments);
  537. }
  538. }
  539. },
  540. // Wrap IDBFS syncfs call with logic to avoid multiple syncs
  541. // running at the same time.
  542. _startSyncFS: function() {
  543. Module._syncInProgress = true;
  544. if (Module._syncMaxTries > Module._syncTries) {
  545. FS.syncfs(false, function(err) {
  546. Module._syncInProgress = false;
  547. if (err) {
  548. console.error("Module._startSyncFS error: " + err);
  549. Module._syncTries += 1;
  550. }
  551. if (Module._syncNeeded) {
  552. Module._syncNeeded = false;
  553. Module._startSyncFS();
  554. }
  555. });
  556. }
  557. },
  558. };
  559. window.onerror = function(err, url, line, column, errObj) {
  560. var errorObject = Module.prepareErrorObject(err, url, line, column, errObj);
  561. Module.ccall('JSWriteDump', 'null', ['string'], [JSON.stringify(errorObject.stack)]);
  562. Module.setStatus('Exception thrown, see JavaScript console');
  563. Module.setStatus = function(text) {
  564. if (text) Module.printErr('[post-exception status] ' + text);
  565. };
  566. };