dmloader.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  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. }
  218. }
  219. };
  220. /* ********************************************************************* */
  221. /* Default input override */
  222. /* ********************************************************************* */
  223. var CanvasInput = {
  224. arrowKeysHandler : function(e) {
  225. switch(e.keyCode) {
  226. case 37: case 38: case 39: case 40: // Arrow keys
  227. case 32: e.preventDefault(); e.stopPropagation(); // Space
  228. default: break; // do not block other keys
  229. }
  230. },
  231. onFocusIn : function(e) {
  232. window.addEventListener("keydown", CanvasInput.arrowKeysHandler, false);
  233. },
  234. onFocusOut: function(e) {
  235. window.removeEventListener("keydown", CanvasInput.arrowKeysHandler, false);
  236. },
  237. addToCanvas : function(canvas) {
  238. canvas.addEventListener("focus", CanvasInput.onFocusIn, false);
  239. canvas.addEventListener("blur", CanvasInput.onFocusOut, false);
  240. canvas.focus();
  241. CanvasInput.onFocusIn();
  242. }
  243. };
  244. /* ********************************************************************* */
  245. /* Module is Emscripten namespace */
  246. /* ********************************************************************* */
  247. var Module = {
  248. noInitialRun: true,
  249. _filesToPreload: [],
  250. _archiveLoaded: false,
  251. _preLoadDone: false,
  252. _waitingForArchive: false,
  253. // Persistent storage
  254. persistentStorage: true,
  255. _syncInProgress: false,
  256. _syncNeeded: false,
  257. _syncInitial: false,
  258. _syncMaxTries: 3,
  259. _syncTries: 0,
  260. print: function(text) { console.log(text); },
  261. printErr: function(text) { console.error(text); },
  262. setStatus: function(text) { console.log(text); },
  263. prepareErrorObject: function (err, url, line, column, errObj) {
  264. line = typeof line == "undefined" ? 0 : line;
  265. column = typeof column == "undefined" ? 0 : column;
  266. url = typeof url == "undefined" ? "" : url;
  267. var errorLine = url + ":" + line + ":" + column;
  268. var error = errObj || (typeof window.event != "undefined" ? window.event.error : "" ) || err || "Undefined Error";
  269. var message = "";
  270. var stack = "";
  271. var backtrace = "";
  272. if (typeof error == "object" && typeof error.stack != "undefined" && typeof error.message != "undefined") {
  273. stack = String(error.stack);
  274. message = String(error.message);
  275. } else {
  276. stack = String(error).split("\n");
  277. message = stack.shift();
  278. stack = stack.join("\n");
  279. }
  280. stack = stack || errorLine;
  281. var callLine = /at (\S+:\d*$)/.exec(message);
  282. if (callLine) {
  283. message = message.replace(/(at \S+:\d*$)/, "");
  284. stack = callLine[1] + "\n" + stack;
  285. }
  286. message = message.replace(/(abort\(.+\)) at .+/, "$1");
  287. stack = stack.replace(/\?{1}\S+(:\d+:\d+)/g, "$1");
  288. stack = stack.replace(/ *at (\S+)$/gm, "@$1");
  289. stack = stack.replace(/ *at (\S+)(?: \[as \S+\])? +\((.+)\)/g, "$1@$2");
  290. stack = stack.replace(/^((?:Object|Array)\.)/gm, "");
  291. stack = stack.split("\n");
  292. return { stack:stack, message:message };
  293. },
  294. hasWebGLSupport: function() {
  295. var webgl_support = false;
  296. try {
  297. var canvas = document.createElement("canvas");
  298. var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
  299. if (gl && gl instanceof WebGLRenderingContext) {
  300. webgl_support = true;
  301. }
  302. } catch (error) {
  303. console.log("An error occurred while detecting WebGL support: " + error);
  304. webgl_support = false;
  305. }
  306. return webgl_support;
  307. },
  308. /**
  309. * Module.runApp - Starts the application given a canvas element id
  310. *
  311. * 'extra_params' is an optional object that can have the following fields:
  312. *
  313. * 'splash_image':
  314. * Path to an image that should be used as a background image for
  315. * the canvas element.
  316. *
  317. * 'archive_location_filter':
  318. * Filter function that will run for each archive path.
  319. *
  320. * 'unsupported_webgl_callback':
  321. * Function that is called if WebGL is not supported.
  322. *
  323. * 'engine_arguments':
  324. * List of arguments (strings) that will be passed to the engine.
  325. *
  326. * 'persistent_storage':
  327. * Boolean toggling the usage of persistent storage.
  328. *
  329. * 'custom_heap_size':
  330. * Number of bytes specifying the memory heap size.
  331. *
  332. **/
  333. runApp: function(app_canvas_id, extra_params) {
  334. app_canvas_id = (typeof app_canvas_id === 'undefined') ? 'canvas' : app_canvas_id;
  335. var params = {
  336. splash_image: undefined,
  337. archive_location_filter: function(path) { return 'split' + path; },
  338. unsupported_webgl_callback: undefined,
  339. engine_arguments: [],
  340. persistent_storage: true,
  341. custom_heap_size: undefined
  342. };
  343. for (var k in extra_params) {
  344. if (extra_params.hasOwnProperty(k)) {
  345. params[k] = extra_params[k];
  346. }
  347. }
  348. Module.canvas = document.getElementById(app_canvas_id);
  349. if (typeof params["splash_image"] !== 'undefined') {
  350. Module.canvas.style.background = 'no-repeat center url("' + params["splash_image"] + '")';
  351. }
  352. Module.arguments = params["engine_arguments"];
  353. Module.persistentStorage = params["persistent_storage"];
  354. Module["TOTAL_MEMORY"] = params["custom_heap_size"];
  355. if (Module.hasWebGLSupport()) {
  356. // Override game keys
  357. CanvasInput.addToCanvas(Module.canvas);
  358. // Load Facebook API
  359. var fb = document.createElement('script');
  360. fb.type = 'text/javascript';
  361. fb.src = '//connect.facebook.net/en_US/sdk.js';
  362. document.head.appendChild(fb);
  363. // Add progress visuals
  364. Progress.addProgress(Module.canvas);
  365. // Load and assemble archive
  366. Combine.addCombineCompletedListener(Module.onArchiveFileLoaded);
  367. Combine.addAllTargetsBuiltListener(Module.onArchiveLoaded);
  368. Combine.addProgressListener(Module.onArchiveLoadProgress);
  369. Combine._archiveLocationFilter = params["archive_location_filter"];
  370. Combine.process(Combine._archiveLocationFilter('/archive_files.json'));
  371. } else {
  372. Progress.addProgress(Module.canvas);
  373. Progress.updateProgress(100, "Unable to start game, WebGL not supported");
  374. Module.setStatus = function(text) {
  375. if (text) Module.printErr('[missing WebGL] ' + text);
  376. };
  377. if (typeof params["unsupported_webgl_callback"] === "function") {
  378. params["unsupported_webgl_callback"]();
  379. }
  380. }
  381. },
  382. onArchiveLoadProgress: function(downloaded, total) {
  383. Progress.updateProgress(downloaded / total * 100);
  384. },
  385. onArchiveFileLoaded: function(name, data) {
  386. Module._filesToPreload.push({path: name, data: data});
  387. },
  388. onArchiveLoaded: function() {
  389. Combine.cleanUp();
  390. Module._archiveLoaded = true;
  391. Progress.updateProgress(100, "Starting...");
  392. if (Module._waitingForArchive) {
  393. Module._preloadAndCallMain();
  394. }
  395. },
  396. toggleFullscreen: function() {
  397. if (GLFW.isFullscreen) {
  398. GLFW.cancelFullScreen();
  399. } else {
  400. GLFW.requestFullScreen();
  401. }
  402. },
  403. preSync: function(done) {
  404. // Initial persistent sync before main is called
  405. FS.syncfs(true, function(err) {
  406. if(err) {
  407. Module._syncTries += 1;
  408. console.error("FS syncfs error: " + err);
  409. if (Module._syncMaxTries > Module._syncTries) {
  410. Module.preSync(done);
  411. } else {
  412. Module._syncInitial = true;
  413. done();
  414. }
  415. } else {
  416. Module._syncInitial = true;
  417. if (done !== undefined) {
  418. done();
  419. }
  420. }
  421. });
  422. },
  423. preloadAll: function() {
  424. if (Module._preLoadDone) {
  425. return;
  426. }
  427. for (var i = 0; i < Module._filesToPreload.length; ++i) {
  428. var item = Module._filesToPreload[i];
  429. FS.createPreloadedFile("", item.path, item.data, true, true);
  430. }
  431. Module._preLoadDone = true;
  432. },
  433. // Tries to do a MEM->IDB sync
  434. // It will flag that another one is needed if there is already one sync running.
  435. persistentSync: function() {
  436. // Need to wait for the initial sync to finish since it
  437. // will call close on all its file streams which will trigger
  438. // new persistentSync for each.
  439. if (Module._syncInitial) {
  440. if (Module._syncInProgress) {
  441. Module._syncNeeded = true;
  442. } else {
  443. Module._startSyncFS();
  444. }
  445. }
  446. },
  447. preInit: [function() {
  448. /* Mount filesystem on preinit */
  449. var dir = DMSYS.GetUserPersistentDataRoot();
  450. FS.mkdir(dir);
  451. // If IndexedDB is supported we mount the persistent data root as IDBFS,
  452. // then try to do a IDB->MEM sync before we start the engine to get
  453. // previously saved data before boot.
  454. window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
  455. if (Module.persistentStorage && window.indexedDB) {
  456. FS.mount(IDBFS, {}, dir);
  457. // Patch FS.close so it will try to sync MEM->IDB
  458. var _close = FS.close; FS.close = function(stream) { var r = _close(stream); Module.persistentSync(); return r; }
  459. // Sync IDB->MEM before calling main()
  460. Module.preSync(function() {
  461. Module._preloadAndCallMain();
  462. });
  463. } else {
  464. Module._preloadAndCallMain();
  465. }
  466. }],
  467. preRun: [function() {
  468. /* If archive is loaded, preload all its files */
  469. if(Module._archiveLoaded) {
  470. Module.preloadAll();
  471. }
  472. }],
  473. postRun: [function() {
  474. if(Module._archiveLoaded) {
  475. Progress.removeProgress();
  476. }
  477. }],
  478. _preloadAndCallMain: function() {
  479. // If the archive isn't loaded,
  480. // we will have to wait with calling main.
  481. if (!Module._archiveLoaded) {
  482. Module._waitingForArchive = true;
  483. } else {
  484. // Need to set heap size before calling main
  485. TOTAL_MEMORY = Module["TOTAL_MEMORY"] || TOTAL_MEMORY;
  486. Module.preloadAll();
  487. Progress.removeProgress();
  488. Module.callMain(Module.arguments);
  489. }
  490. },
  491. // Wrap IDBFS syncfs call with logic to avoid multiple syncs
  492. // running at the same time.
  493. _startSyncFS: function() {
  494. Module._syncInProgress = true;
  495. if (Module._syncMaxTries > Module._syncTries) {
  496. FS.syncfs(false, function(err) {
  497. Module._syncInProgress = false;
  498. if (err) {
  499. console.error("Module._startSyncFS error: " + err);
  500. Module._syncTries += 1;
  501. }
  502. if (Module._syncNeeded) {
  503. Module._syncNeeded = false;
  504. Module._startSyncFS();
  505. }
  506. });
  507. }
  508. },
  509. };
  510. window.onerror = function(err, url, line, column, errObj) {
  511. var errorObject = Module.prepareErrorObject(err, url, line, column, errObj);
  512. Module.ccall('JSWriteDump', 'null', ['string'], [JSON.stringify(errorObject.stack)]);
  513. Module.setStatus('Exception thrown, see JavaScript console');
  514. Module.setStatus = function(text) {
  515. if (text) Module.printErr('[post-exception status] ' + text);
  516. };
  517. };