/** * An object used to configure the Engine instance based on godot export options, and to override those in custom HTML * templates if needed. * * @header Engine configuration * @summary The Engine configuration object. This is just a typedef, create it like a regular object, e.g.: * * ``const MyConfig = { executable: 'godot', unloadAfterInit: false }`` * * @typedef {Object} EngineConfig */ const EngineConfig = {}; // eslint-disable-line no-unused-vars /** * @struct * @constructor * @ignore */ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-vars const cfg = /** @lends {InternalConfig.prototype} */ { /** * Whether the unload the engine automatically after the instance is initialized. * * @memberof EngineConfig * @default * @type {boolean} */ unloadAfterInit: true, /** * The HTML DOM Canvas object to use. * * By default, the first canvas element in the document will be used is none is specified. * * @memberof EngineConfig * @default * @type {?HTMLCanvasElement} */ canvas: null, /** * The name of the WASM file without the extension. (Set by Godot Editor export process). * * @memberof EngineConfig * @default * @type {string} */ executable: '', /** * An alternative name for the game pck to load. The executable name is used otherwise. * * @memberof EngineConfig * @default * @type {?string} */ mainPack: null, /** * Specify a language code to select the proper localization for the game. * * The browser locale will be used if none is specified. See complete list of * :ref:`supported locales `. * * @memberof EngineConfig * @type {?string} * @default */ locale: null, /** * The canvas resize policy determines how the canvas should be resized by Godot. * * ``0`` means Godot won't do any resizing. This is useful if you want to control the canvas size from * javascript code in your template. * * ``1`` means Godot will resize the canvas on start, and when changing window size via engine functions. * * ``2`` means Godot will adapt the canvas size to match the whole browser window. * * @memberof EngineConfig * @type {number} * @default */ canvasResizePolicy: 2, /** * The arguments to be passed as command line arguments on startup. * * See :ref:`command line tutorial `. * * **Note**: :js:meth:`startGame ` will always add the ``--main-pack`` argument. * * @memberof EngineConfig * @type {Array} * @default */ args: [], /** * When enabled, the game canvas will automatically grab the focus when the engine starts. * * @memberof EngineConfig * @type {boolean} * @default */ focusCanvas: true, /** * When enabled, this will turn on experimental virtual keyboard support on mobile. * * @memberof EngineConfig * @type {boolean} * @default */ experimentalVK: false, /** * The progressive web app service worker to install. * @memberof EngineConfig * @default * @type {string} */ serviceWorker: '', /** * @ignore * @type {Array.} */ persistentPaths: ['/userfs'], /** * @ignore * @type {boolean} */ persistentDrops: false, /** * @ignore * @type {Array.} */ gdextensionLibs: [], /** * @ignore * @type {Array.} */ fileSizes: [], /** * A callback function for handling Godot's ``OS.execute`` calls. * * This is for example used in the Web Editor template to switch between project manager and editor, and for running the game. * * @callback EngineConfig.onExecute * @param {string} path The path that Godot's wants executed. * @param {Array.} args The arguments of the "command" to execute. */ /** * @ignore * @type {?function(string, Array.)} */ onExecute: null, /** * A callback function for being notified when the Godot instance quits. * * **Note**: This function will not be called if the engine crashes or become unresponsive. * * @callback EngineConfig.onExit * @param {number} status_code The status code returned by Godot on exit. */ /** * @ignore * @type {?function(number)} */ onExit: null, /** * A callback function for displaying download progress. * * The function is called once per frame while downloading files, so the usage of ``requestAnimationFrame()`` * is not necessary. * * If the callback function receives a total amount of bytes as 0, this means that it is impossible to calculate. * Possible reasons include: * * - Files are delivered with server-side chunked compression * - Files are delivered with server-side compression on Chromium * - Not all file downloads have started yet (usually on servers without multi-threading) * * @callback EngineConfig.onProgress * @param {number} current The current amount of downloaded bytes so far. * @param {number} total The total amount of bytes to be downloaded. */ /** * @ignore * @type {?function(number, number)} */ onProgress: null, /** * A callback function for handling the standard output stream. This method should usually only be used in debug pages. * * By default, ``console.log()`` is used. * * @callback EngineConfig.onPrint * @param {...*} [var_args] A variadic number of arguments to be printed. */ /** * @ignore * @type {?function(...*)} */ onPrint: function () { console.log.apply(console, Array.from(arguments)); // eslint-disable-line no-console }, /** * A callback function for handling the standard error stream. This method should usually only be used in debug pages. * * By default, ``console.error()`` is used. * * @callback EngineConfig.onPrintError * @param {...*} [var_args] A variadic number of arguments to be printed as errors. */ /** * @ignore * @type {?function(...*)} */ onPrintError: function (var_args) { console.error.apply(console, Array.from(arguments)); // eslint-disable-line no-console }, }; /** * @ignore * @struct * @constructor * @param {EngineConfig} opts */ function Config(opts) { this.update(opts); } Config.prototype = cfg; /** * @ignore * @param {EngineConfig} opts */ Config.prototype.update = function (opts) { const config = opts || {}; // NOTE: We must explicitly pass the default, accessing it via // the key will fail due to closure compiler renames. function parse(key, def) { if (typeof (config[key]) === 'undefined') { return def; } return config[key]; } // Module config this.unloadAfterInit = parse('unloadAfterInit', this.unloadAfterInit); this.onPrintError = parse('onPrintError', this.onPrintError); this.onPrint = parse('onPrint', this.onPrint); this.onProgress = parse('onProgress', this.onProgress); // Godot config this.canvas = parse('canvas', this.canvas); this.executable = parse('executable', this.executable); this.mainPack = parse('mainPack', this.mainPack); this.locale = parse('locale', this.locale); this.canvasResizePolicy = parse('canvasResizePolicy', this.canvasResizePolicy); this.persistentPaths = parse('persistentPaths', this.persistentPaths); this.persistentDrops = parse('persistentDrops', this.persistentDrops); this.experimentalVK = parse('experimentalVK', this.experimentalVK); this.focusCanvas = parse('focusCanvas', this.focusCanvas); this.serviceWorker = parse('serviceWorker', this.serviceWorker); this.gdextensionLibs = parse('gdextensionLibs', this.gdextensionLibs); this.fileSizes = parse('fileSizes', this.fileSizes); this.args = parse('args', this.args); this.onExecute = parse('onExecute', this.onExecute); this.onExit = parse('onExit', this.onExit); }; /** * @ignore * @param {string} loadPath * @param {Response} response */ Config.prototype.getModuleConfig = function (loadPath, response) { let r = response; return { 'print': this.onPrint, 'printErr': this.onPrintError, 'thisProgram': this.executable, 'noExitRuntime': false, 'dynamicLibraries': [`${loadPath}.side.wasm`], 'instantiateWasm': function (imports, onSuccess) { function done(result) { onSuccess(result['instance'], result['module']); } if (typeof (WebAssembly.instantiateStreaming) !== 'undefined') { WebAssembly.instantiateStreaming(Promise.resolve(r), imports).then(done); } else { r.arrayBuffer().then(function (buffer) { WebAssembly.instantiate(buffer, imports).then(done); }); } r = null; return {}; }, 'locateFile': function (path) { if (path.endsWith('.worker.js')) { return `${loadPath}.worker.js`; } else if (path.endsWith('.audio.worklet.js')) { return `${loadPath}.audio.worklet.js`; } else if (path.endsWith('.js')) { return `${loadPath}.js`; } else if (path.endsWith('.side.wasm')) { return `${loadPath}.side.wasm`; } else if (path.endsWith('.wasm')) { return `${loadPath}.wasm`; } return path; }, }; }; /** * @ignore * @param {function()} cleanup */ Config.prototype.getGodotConfig = function (cleanup) { // Try to find a canvas if (!(this.canvas instanceof HTMLCanvasElement)) { const nodes = document.getElementsByTagName('canvas'); if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { const first = nodes[0]; this.canvas = /** @type {!HTMLCanvasElement} */ (first); } if (!this.canvas) { throw new Error('No canvas found in page'); } } // Canvas can grab focus on click, or key events won't work. if (this.canvas.tabIndex < 0) { this.canvas.tabIndex = 0; } // Browser locale, or custom one if defined. let locale = this.locale; if (!locale) { locale = navigator.languages ? navigator.languages[0] : navigator.language; locale = locale.split('.')[0]; } locale = locale.replace('-', '_'); const onExit = this.onExit; // Godot configuration. return { 'canvas': this.canvas, 'canvasResizePolicy': this.canvasResizePolicy, 'locale': locale, 'persistentDrops': this.persistentDrops, 'virtualKeyboard': this.experimentalVK, 'focusCanvas': this.focusCanvas, 'onExecute': this.onExecute, 'onExit': function (p_code) { cleanup(); // We always need to call the cleanup callback to free memory. if (typeof (onExit) === 'function') { onExit(p_code); } }, }; }; return new Config(initConfig); };