diff options
Diffstat (limited to 'platform/javascript/js')
-rw-r--r-- | platform/javascript/js/engine/config.js | 337 | ||||
-rw-r--r-- | platform/javascript/js/engine/engine.externs.js | 1 | ||||
-rw-r--r-- | platform/javascript/js/engine/engine.js | 485 | ||||
-rw-r--r-- | platform/javascript/js/engine/preloader.js | 125 | ||||
-rw-r--r-- | platform/javascript/js/engine/utils.js | 58 | ||||
-rw-r--r-- | platform/javascript/js/jsdoc2rst/publish.js | 354 | ||||
-rw-r--r-- | platform/javascript/js/libs/library_godot_audio.js | 5 | ||||
-rw-r--r-- | platform/javascript/js/libs/library_godot_display.js | 394 | ||||
-rw-r--r-- | platform/javascript/js/libs/library_godot_fetch.js | 258 | ||||
-rw-r--r-- | platform/javascript/js/libs/library_godot_http_request.js | 142 | ||||
-rw-r--r-- | platform/javascript/js/libs/library_godot_os.js | 44 | ||||
-rw-r--r-- | platform/javascript/js/libs/library_godot_runtime.js | 16 |
12 files changed, 1661 insertions, 558 deletions
diff --git a/platform/javascript/js/engine/config.js b/platform/javascript/js/engine/config.js new file mode 100644 index 0000000000..6072782875 --- /dev/null +++ b/platform/javascript/js/engine/config.js @@ -0,0 +1,337 @@ +/** + * 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 <doc_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 <doc_command_line_tutorial>`. + * + * **Note**: :js:meth:`startGame <Engine.prototype.startGame>` will always add the ``--main-pack`` argument. + * + * @memberof EngineConfig + * @type {Array<string>} + * @default + */ + args: [], + /** + * When enabled, this will turn on experimental virtual keyboard support on mobile. + * + * @memberof EngineConfig + * @type {boolean} + * @default + */ + experimentalVK: false, + /** + * @ignore + * @type {Array.<string>} + */ + persistentPaths: ['/userfs'], + /** + * @ignore + * @type {boolean} + */ + persistentDrops: false, + /** + * @ignore + * @type {Array.<string>} + */ + gdnativeLibs: [], + /** + * @ignore + * @type {Array.<string>} + */ + 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.<string>} args The arguments of the "command" to execute. + */ + /** + * @ignore + * @type {?function(string, Array.<string>)} + */ + 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 || {}; + 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.gdnativeLibs = parse('gdnativeLibs', this.gdnativeLibs); + 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': true, + '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) { + this.canvas = nodes[0]; + } + 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]; + } + const onExit = this.onExit; + + // Godot configuration. + return { + 'canvas': this.canvas, + 'canvasResizePolicy': this.canvasResizePolicy, + 'locale': locale, + 'persistentDrops': this.persistentDrops, + 'virtualKeyboard': this.experimentalVK, + '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); +}; diff --git a/platform/javascript/js/engine/engine.externs.js b/platform/javascript/js/engine/engine.externs.js index 1a94dd15ec..35a66a93ae 100644 --- a/platform/javascript/js/engine/engine.externs.js +++ b/platform/javascript/js/engine/engine.externs.js @@ -1,3 +1,4 @@ var Godot; var WebAssembly = {}; WebAssembly.instantiate = function(buffer, imports) {}; +WebAssembly.instantiateStreaming = function(response, imports) {}; diff --git a/platform/javascript/js/engine/engine.js b/platform/javascript/js/engine/engine.js index 321221323c..19a0552c8c 100644 --- a/platform/javascript/js/engine/engine.js +++ b/platform/javascript/js/engine/engine.js @@ -1,280 +1,273 @@ +/** + * Projects exported for the Web expose the :js:class:`Engine` class to the JavaScript environment, that allows + * fine control over the engine's start-up process. + * + * This API is built in an asynchronous manner and requires basic understanding + * of `Promises <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises>`__. + * + * @module Engine + * @header HTML5 shell class reference + */ const Engine = (function () { const preloader = new Preloader(); - let wasmExt = '.wasm'; - let unloadAfterInit = true; - let loadPath = ''; let loadPromise = null; + let loadPath = ''; let initPromise = null; - let stderr = null; - let stdout = null; - let progressFunc = null; - function load(basePath) { + /** + * @classdesc The ``Engine`` class provides methods for loading and starting exported projects on the Web. For default export + * settings, this is already part of the exported HTML page. To understand practical use of the ``Engine`` class, + * see :ref:`Custom HTML page for Web export <doc_customizing_html5_shell>`. + * + * @description Create a new Engine instance with the given configuration. + * + * @global + * @constructor + * @param {EngineConfig} initConfig The initial config for this instance. + */ + function Engine(initConfig) { // eslint-disable-line no-shadow + this.config = new InternalConfig(initConfig); + this.rtenv = null; + } + + /** + * Load the engine from the specified base path. + * + * @param {string} basePath Base path of the engine to load. + * @param {number=} [size=0] The file size if known. + * @returns {Promise} A Promise that resolves once the engine is loaded. + * + * @function Engine.load + */ + Engine.load = function (basePath, size) { if (loadPromise == null) { loadPath = basePath; - loadPromise = preloader.loadPromise(basePath + wasmExt); - preloader.setProgressFunc(progressFunc); + loadPromise = preloader.loadPromise(`${loadPath}.wasm`, size, true); requestAnimationFrame(preloader.animateProgress); } return loadPromise; - } + }; - function unload() { + /** + * Unload the engine to free memory. + * + * This method will be called automatically depending on the configuration. See :js:attr:`unloadAfterInit`. + * + * @function Engine.unload + */ + Engine.unload = function () { loadPromise = null; - } - - /** @constructor */ - function Engine() { // eslint-disable-line no-shadow - this.canvas = null; - this.executableName = ''; - this.rtenv = null; - this.customLocale = null; - this.resizeCanvasOnStart = false; - this.onExecute = null; - this.onExit = null; - this.persistentPaths = ['/userfs']; - this.gdnativeLibs = []; - } - - Engine.prototype.init = /** @param {string=} basePath */ function (basePath) { - if (initPromise) { - return initPromise; - } - if (loadPromise == null) { - if (!basePath) { - initPromise = Promise.reject(new Error('A base path must be provided when calling `init` and the engine is not loaded.')); - return initPromise; - } - load(basePath); - } - let config = {}; - if (typeof stdout === 'function') { - config.print = stdout; - } - if (typeof stderr === 'function') { - config.printErr = stderr; - } - const me = this; - initPromise = new Promise(function (resolve, reject) { - config['locateFile'] = Utils.createLocateRewrite(loadPath); - config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); - // Emscripten configuration. - config['thisProgram'] = me.executableName; - config['noExitRuntime'] = true; - config['dynamicLibraries'] = [`${me.executableName}.side.wasm`].concat(me.gdnativeLibs); - Godot(config).then(function (module) { - module['initFS'](me.persistentPaths).then(function (fs_err) { - me.rtenv = module; - if (unloadAfterInit) { - unload(); - } - resolve(); - config = null; - }); - }); - }); - return initPromise; }; - /** @type {function(string, string):Object} */ - Engine.prototype.preloadFile = function (file, path) { - return preloader.preload(file, path); + /** + * Check whether WebGL is available. Optionally, specify a particular version of WebGL to check for. + * + * @param {number=} [majorVersion=1] The major WebGL version to check for. + * @returns {boolean} If the given major version of WebGL is available. + * @function Engine.isWebGLAvailable + */ + Engine.isWebGLAvailable = function (majorVersion = 1) { + try { + return !!document.createElement('canvas').getContext(['webgl', 'webgl2'][majorVersion - 1]); + } catch (e) { /* Not available */ } + return false; }; - /** @type {function(...string):Object} */ - Engine.prototype.start = function () { - // Start from arguments. - const args = []; - for (let i = 0; i < arguments.length; i++) { - args.push(arguments[i]); - } - const me = this; - return me.init().then(function () { - if (!me.rtenv) { - return Promise.reject(new Error('The engine must be initialized before it can be started')); - } - - if (!(me.canvas instanceof HTMLCanvasElement)) { - me.canvas = Utils.findCanvas(); - if (!me.canvas) { - return Promise.reject(new Error('No canvas found in page')); + /** + * Safe Engine constructor, creates a new prototype for every new instance to avoid prototype pollution. + * @ignore + * @constructor + */ + function SafeEngine(initConfig) { + const proto = /** @lends Engine.prototype */ { + /** + * Initialize the engine instance. Optionally, pass the base path to the engine to load it, + * if it hasn't been loaded yet. See :js:meth:`Engine.load`. + * + * @param {string=} basePath Base path of the engine to load. + * @return {Promise} A ``Promise`` that resolves once the engine is loaded and initialized. + */ + init: function (basePath) { + if (initPromise) { + return initPromise; + } + if (loadPromise == null) { + if (!basePath) { + initPromise = Promise.reject(new Error('A base path must be provided when calling `init` and the engine is not loaded.')); + return initPromise; + } + Engine.load(basePath, this.config.fileSizes[`${basePath}.wasm`]); + } + const me = this; + function doInit(promise) { + return promise.then(function (response) { + return Godot(me.config.getModuleConfig(loadPath, response.clone())); + }).then(function (module) { + const paths = me.config.persistentPaths; + return module['initFS'](paths).then(function (err) { + return Promise.resolve(module); + }); + }).then(function (module) { + me.rtenv = module; + if (me.config.unloadAfterInit) { + Engine.unload(); + } + return Promise.resolve(); + }); } - } + preloader.setProgressFunc(this.config.onProgress); + initPromise = doInit(loadPromise); + return initPromise; + }, - // Canvas can grab focus on click, or key events won't work. - if (me.canvas.tabIndex < 0) { - me.canvas.tabIndex = 0; - } + /** + * Load a file so it is available in the instance's file system once it runs. Must be called **before** starting the + * instance. + * + * If not provided, the ``path`` is derived from the URL of the loaded file. + * + * @param {string|ArrayBuffer} file The file to preload. + * + * If a ``string`` the file will be loaded from that path. + * + * If an ``ArrayBuffer`` or a view on one, the buffer will used as the content of the file. + * + * @param {string=} path Path by which the file will be accessible. Required, if ``file`` is not a string. + * + * @returns {Promise} A Promise that resolves once the file is loaded. + */ + preloadFile: function (file, path) { + return preloader.preload(file, path, this.config.fileSizes[file]); + }, - // Browser locale, or custom one if defined. - let locale = me.customLocale; - if (!locale) { - locale = navigator.languages ? navigator.languages[0] : navigator.language; - locale = locale.split('.')[0]; - } - // Godot configuration. - me.rtenv['initConfig']({ - 'resizeCanvasOnStart': me.resizeCanvasOnStart, - 'canvas': me.canvas, - 'locale': locale, - 'onExecute': function (p_args) { - if (me.onExecute) { - me.onExecute(p_args); - return 0; + /** + * Start the engine instance using the given override configuration (if any). + * :js:meth:`startGame <Engine.prototype.startGame>` can be used in typical cases instead. + * + * This will initialize the instance if it is not initialized. For manual initialization, see :js:meth:`init <Engine.prototype.init>`. + * The engine must be loaded beforehand. + * + * Fails if a canvas cannot be found on the page, or not specified in the configuration. + * + * @param {EngineConfig} override An optional configuration override. + * @return {Promise} Promise that resolves once the engine started. + */ + start: function (override) { + this.config.update(override); + const me = this; + return me.init().then(function () { + if (!me.rtenv) { + return Promise.reject(new Error('The engine must be initialized before it can be started')); } - return 1; - }, - 'onExit': function (p_code) { - me.rtenv['deinitFS'](); - if (me.onExit) { - me.onExit(p_code); + + let config = {}; + try { + config = me.config.getGodotConfig(function () { + me.rtenv = null; + }); + } catch (e) { + return Promise.reject(e); } - me.rtenv = null; - }, - }); + // Godot configuration. + me.rtenv['initConfig'](config); - return new Promise(function (resolve, reject) { - preloader.preloadedFiles.forEach(function (file) { - me.rtenv['copyToFS'](file.path, file.buffer); + // Preload GDNative libraries. + const libs = []; + me.config.gdnativeLibs.forEach(function (lib) { + libs.push(me.rtenv['loadDynamicLibrary'](lib, { 'loadAsync': true })); + }); + return Promise.all(libs).then(function () { + return new Promise(function (resolve, reject) { + preloader.preloadedFiles.forEach(function (file) { + me.rtenv['copyToFS'](file.path, file.buffer); + }); + preloader.preloadedFiles.length = 0; // Clear memory + me.rtenv['callMain'](me.config.args); + initPromise = null; + resolve(); + }); + }); }); - preloader.preloadedFiles.length = 0; // Clear memory - me.rtenv['callMain'](args); - initPromise = null; - resolve(); - }); - }); - }; - - Engine.prototype.startGame = function (execName, mainPack, extraArgs) { - // Start and init with execName as loadPath if not inited. - this.executableName = execName; - const me = this; - return Promise.all([ - this.init(execName), - this.preloadFile(mainPack, mainPack), - ]).then(function () { - let args = ['--main-pack', mainPack]; - if (extraArgs) { - args = args.concat(extraArgs); - } - return me.start.apply(me, args); - }); - }; - - Engine.prototype.setWebAssemblyFilenameExtension = function (override) { - if (String(override).length === 0) { - throw new Error('Invalid WebAssembly filename extension override'); - } - wasmExt = String(override); - }; + }, - Engine.prototype.setUnloadAfterInit = function (enabled) { - unloadAfterInit = enabled; - }; - - Engine.prototype.setCanvas = function (canvasElem) { - this.canvas = canvasElem; - }; - - Engine.prototype.setCanvasResizedOnStart = function (enabled) { - this.resizeCanvasOnStart = enabled; - }; - - Engine.prototype.setLocale = function (locale) { - this.customLocale = locale; - }; - - Engine.prototype.setExecutableName = function (newName) { - this.executableName = newName; - }; - - Engine.prototype.setProgressFunc = function (func) { - progressFunc = func; - }; + /** + * Start the game instance using the given configuration override (if any). + * + * This will initialize the instance if it is not initialized. For manual initialization, see :js:meth:`init <Engine.prototype.init>`. + * + * This will load the engine if it is not loaded, and preload the main pck. + * + * This method expects the initial config (or the override) to have both the :js:attr:`executable` and :js:attr:`mainPack` + * properties set (normally done by the editor during export). + * + * @param {EngineConfig} override An optional configuration override. + * @return {Promise} Promise that resolves once the game started. + */ + startGame: function (override) { + this.config.update(override); + // Add main-pack argument. + const exe = this.config.executable; + const pack = this.config.mainPack || `${exe}.pck`; + this.config.args = ['--main-pack', pack].concat(this.config.args); + // Start and init with execName as loadPath if not inited. + const me = this; + return Promise.all([ + this.init(exe), + this.preloadFile(pack, pack), + ]).then(function () { + return me.start.apply(me); + }); + }, - Engine.prototype.setStdoutFunc = function (func) { - const print = function (text) { - let msg = text; - if (arguments.length > 1) { - msg = Array.prototype.slice.call(arguments).join(' '); - } - func(msg); - }; - if (this.rtenv) { - this.rtenv.print = print; - } - stdout = print; - }; + /** + * Create a file at the specified ``path`` with the passed as ``buffer`` in the instance's file system. + * + * @param {string} path The location where the file will be created. + * @param {ArrayBuffer} buffer The content of the file. + */ + copyToFS: function (path, buffer) { + if (this.rtenv == null) { + throw new Error('Engine must be inited before copying files'); + } + this.rtenv['copyToFS'](path, buffer); + }, - Engine.prototype.setStderrFunc = function (func) { - const printErr = function (text) { - let msg = text; - if (arguments.length > 1) { - msg = Array.prototype.slice.call(arguments).join(' '); - } - func(msg); + /** + * Request that the current instance quit. + * + * This is akin the user pressing the close button in the window manager, and will + * have no effect if the engine has crashed, or is stuck in a loop. + * + */ + requestQuit: function () { + if (this.rtenv) { + this.rtenv['request_quit'](); + } + }, }; - if (this.rtenv) { - this.rtenv.printErr = printErr; - } - stderr = printErr; - }; - - Engine.prototype.setOnExecute = function (onExecute) { - this.onExecute = onExecute; - }; - - Engine.prototype.setOnExit = function (onExit) { - this.onExit = onExit; - }; - - Engine.prototype.copyToFS = function (path, buffer) { - if (this.rtenv == null) { - throw new Error('Engine must be inited before copying files'); - } - this.rtenv['copyToFS'](path, buffer); - }; - - Engine.prototype.setPersistentPaths = function (persistentPaths) { - this.persistentPaths = persistentPaths; - }; - Engine.prototype.setGDNativeLibraries = function (gdnativeLibs) { - this.gdnativeLibs = gdnativeLibs; - }; + Engine.prototype = proto; + // Closure compiler exported instance methods. + Engine.prototype['init'] = Engine.prototype.init; + Engine.prototype['preloadFile'] = Engine.prototype.preloadFile; + Engine.prototype['start'] = Engine.prototype.start; + Engine.prototype['startGame'] = Engine.prototype.startGame; + Engine.prototype['copyToFS'] = Engine.prototype.copyToFS; + Engine.prototype['requestQuit'] = Engine.prototype.requestQuit; + // Also expose static methods as instance methods + Engine.prototype['load'] = Engine.load; + Engine.prototype['unload'] = Engine.unload; + Engine.prototype['isWebGLAvailable'] = Engine.isWebGLAvailable; + return new Engine(initConfig); + } - Engine.prototype.requestQuit = function () { - if (this.rtenv) { - this.rtenv['request_quit'](); - } - }; + // Closure compiler exported static methods. + SafeEngine['load'] = Engine.load; + SafeEngine['unload'] = Engine.unload; + SafeEngine['isWebGLAvailable'] = Engine.isWebGLAvailable; - // Closure compiler exported engine methods. - /** @export */ - Engine['isWebGLAvailable'] = Utils.isWebGLAvailable; - Engine['load'] = load; - Engine['unload'] = unload; - Engine.prototype['init'] = Engine.prototype.init; - Engine.prototype['preloadFile'] = Engine.prototype.preloadFile; - Engine.prototype['start'] = Engine.prototype.start; - Engine.prototype['startGame'] = Engine.prototype.startGame; - Engine.prototype['setWebAssemblyFilenameExtension'] = Engine.prototype.setWebAssemblyFilenameExtension; - Engine.prototype['setUnloadAfterInit'] = Engine.prototype.setUnloadAfterInit; - Engine.prototype['setCanvas'] = Engine.prototype.setCanvas; - Engine.prototype['setCanvasResizedOnStart'] = Engine.prototype.setCanvasResizedOnStart; - Engine.prototype['setLocale'] = Engine.prototype.setLocale; - Engine.prototype['setExecutableName'] = Engine.prototype.setExecutableName; - Engine.prototype['setProgressFunc'] = Engine.prototype.setProgressFunc; - Engine.prototype['setStdoutFunc'] = Engine.prototype.setStdoutFunc; - Engine.prototype['setStderrFunc'] = Engine.prototype.setStderrFunc; - Engine.prototype['setOnExecute'] = Engine.prototype.setOnExecute; - Engine.prototype['setOnExit'] = Engine.prototype.setOnExit; - Engine.prototype['copyToFS'] = Engine.prototype.copyToFS; - Engine.prototype['setPersistentPaths'] = Engine.prototype.setPersistentPaths; - Engine.prototype['setGDNativeLibraries'] = Engine.prototype.setGDNativeLibraries; - Engine.prototype['requestQuit'] = Engine.prototype.requestQuit; - return Engine; + return SafeEngine; }()); if (typeof window !== 'undefined') { window['Engine'] = Engine; diff --git a/platform/javascript/js/engine/preloader.js b/platform/javascript/js/engine/preloader.js index ec34fb93f2..3535fdb361 100644 --- a/platform/javascript/js/engine/preloader.js +++ b/platform/javascript/js/engine/preloader.js @@ -1,54 +1,79 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars - const loadXHR = function (resolve, reject, file, tracker, attempts) { - const xhr = new XMLHttpRequest(); + function getTrackedResponse(response, load_status) { + let clen = 0; + let compressed = false; + response.headers.forEach(function (value, header) { + const h = header.toLowerCase().trim(); + // We can't accurately compute compressed stream length. + if (h === 'content-encoding') { + compressed = true; + } else if (h === 'content-length') { + const length = parseInt(value, 10); + if (!Number.isNaN(length) && length > 0) { + clen = length; + } + } + }); + if (!compressed && clen) { + load_status.total = clen; + } + function onloadprogress(reader, controller) { + return reader.read().then(function (result) { + if (load_status.done) { + return Promise.resolve(); + } + if (result.value) { + controller.enqueue(result.value); + load_status.loaded += result.value.length; + } + if (!result.done) { + return onloadprogress(reader, controller); + } + load_status.done = true; + return Promise.resolve(); + }); + } + const reader = response.body.getReader(); + return new Response(new ReadableStream({ + start: function (controller) { + onloadprogress(reader, controller).then(function () { + controller.close(); + }); + }, + }), { headers: response.headers }); + } + + function loadFetch(file, tracker, fileSize, raw) { tracker[file] = { - total: 0, + total: fileSize || 0, loaded: 0, - final: false, + done: false, }; - xhr.onerror = function () { + return fetch(file).then(function (response) { + if (!response.ok) { + return Promise.reject(new Error(`Failed loading file '${file}'`)); + } + const tr = getTrackedResponse(response, tracker[file]); + if (raw) { + return Promise.resolve(tr); + } + return tr.arrayBuffer(); + }); + } + + function retry(func, attempts = 1) { + function onerror(err) { if (attempts <= 1) { - reject(new Error(`Failed loading file '${file}'`)); - } else { + return Promise.reject(err); + } + return new Promise(function (resolve, reject) { setTimeout(function () { - loadXHR(resolve, reject, file, tracker, attempts - 1); + retry(func, attempts - 1).then(resolve).catch(reject); }, 1000); - } - }; - xhr.onabort = function () { - tracker[file].final = true; - reject(new Error(`Loading file '${file}' was aborted.`)); - }; - xhr.onloadstart = function (ev) { - tracker[file].total = ev.total; - tracker[file].loaded = ev.loaded; - }; - xhr.onprogress = function (ev) { - tracker[file].loaded = ev.loaded; - tracker[file].total = ev.total; - }; - xhr.onload = function () { - if (xhr.status >= 400) { - if (xhr.status < 500 || attempts <= 1) { - reject(new Error(`Failed loading file '${file}': ${xhr.statusText}`)); - xhr.abort(); - } else { - setTimeout(function () { - loadXHR(resolve, reject, file, tracker, attempts - 1); - }, 1000); - } - } else { - tracker[file].final = true; - resolve(xhr); - } - }; - // Make request. - xhr.open('GET', file); - if (!file.endsWith('.js')) { - xhr.responseType = 'arraybuffer'; + }); } - xhr.send(); - }; + return func().catch(onerror); + } const DOWNLOAD_ATTEMPTS_MAX = 4; const loadingFiles = {}; @@ -63,7 +88,7 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un Object.keys(loadingFiles).forEach(function (file) { const stat = loadingFiles[file]; - if (!stat.final) { + if (!stat.done) { progressIsFinal = false; } if (!totalIsValid || stat.total === 0) { @@ -92,21 +117,19 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un progressFunc = callback; }; - this.loadPromise = function (file) { - return new Promise(function (resolve, reject) { - loadXHR(resolve, reject, file, loadingFiles, DOWNLOAD_ATTEMPTS_MAX); - }); + this.loadPromise = function (file, fileSize, raw = false) { + return retry(loadFetch.bind(null, file, loadingFiles, fileSize, raw), DOWNLOAD_ATTEMPTS_MAX); }; this.preloadedFiles = []; - this.preload = function (pathOrBuffer, destPath) { + this.preload = function (pathOrBuffer, destPath, fileSize) { let buffer = null; if (typeof pathOrBuffer === 'string') { const me = this; - return this.loadPromise(pathOrBuffer).then(function (xhr) { + return this.loadPromise(pathOrBuffer, fileSize).then(function (buf) { me.preloadedFiles.push({ path: destPath || pathOrBuffer, - buffer: xhr.response, + buffer: buf, }); return Promise.resolve(); }); diff --git a/platform/javascript/js/engine/utils.js b/platform/javascript/js/engine/utils.js deleted file mode 100644 index 9273bbad42..0000000000 --- a/platform/javascript/js/engine/utils.js +++ /dev/null @@ -1,58 +0,0 @@ -const Utils = { // eslint-disable-line no-unused-vars - - createLocateRewrite: function (execName) { - function rw(path) { - if (path.endsWith('.worker.js')) { - return `${execName}.worker.js`; - } else if (path.endsWith('.audio.worklet.js')) { - return `${execName}.audio.worklet.js`; - } else if (path.endsWith('.js')) { - return `${execName}.js`; - } else if (path.endsWith('.side.wasm')) { - return `${execName}.side.wasm`; - } else if (path.endsWith('.wasm')) { - return `${execName}.wasm`; - } - return path; - } - return rw; - }, - - createInstantiatePromise: function (wasmLoader) { - let loader = wasmLoader; - function instantiateWasm(imports, onSuccess) { - loader.then(function (xhr) { - WebAssembly.instantiate(xhr.response, imports).then(function (result) { - onSuccess(result['instance'], result['module']); - }); - }); - loader = null; - return {}; - } - - return instantiateWasm; - }, - - findCanvas: function () { - const nodes = document.getElementsByTagName('canvas'); - if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { - return nodes[0]; - } - return null; - }, - - isWebGLAvailable: function (majorVersion = 1) { - let testContext = false; - try { - const testCanvas = document.createElement('canvas'); - if (majorVersion === 1) { - testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); - } else if (majorVersion === 2) { - testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2'); - } - } catch (e) { - // Not available - } - return !!testContext; - }, -}; diff --git a/platform/javascript/js/jsdoc2rst/publish.js b/platform/javascript/js/jsdoc2rst/publish.js new file mode 100644 index 0000000000..ad9c0fbaaa --- /dev/null +++ b/platform/javascript/js/jsdoc2rst/publish.js @@ -0,0 +1,354 @@ +/* eslint-disable strict */ + +'use strict'; + +const fs = require('fs'); + +class JSDoclet { + constructor(doc) { + this.doc = doc; + this.description = doc['description'] || ''; + this.name = doc['name'] || 'unknown'; + this.longname = doc['longname'] || ''; + this.types = []; + if (doc['type'] && doc['type']['names']) { + this.types = doc['type']['names'].slice(); + } + this.type = this.types.length > 0 ? this.types.join('\\|') : '*'; + this.variable = doc['variable'] || false; + this.kind = doc['kind'] || ''; + this.memberof = doc['memberof'] || null; + this.scope = doc['scope'] || ''; + this.members = []; + this.optional = doc['optional'] || false; + this.defaultvalue = doc['defaultvalue']; + this.summary = doc['summary'] || null; + this.classdesc = doc['classdesc'] || null; + + // Parameters (functions) + this.params = []; + this.returns = doc['returns'] ? doc['returns'][0]['type']['names'][0] : 'void'; + this.returns_desc = doc['returns'] ? doc['returns'][0]['description'] : null; + + this.params = (doc['params'] || []).slice().map((p) => new JSDoclet(p)); + + // Custom tags + this.tags = doc['tags'] || []; + this.header = this.tags.filter((t) => t['title'] === 'header').map((t) => t['text']).pop() || null; + } + + add_member(obj) { + this.members.push(obj); + } + + is_static() { + return this.scope === 'static'; + } + + is_instance() { + return this.scope === 'instance'; + } + + is_object() { + return this.kind === 'Object' || (this.kind === 'typedef' && this.type === 'Object'); + } + + is_class() { + return this.kind === 'class'; + } + + is_function() { + return this.kind === 'function' || (this.kind === 'typedef' && this.type === 'function'); + } + + is_module() { + return this.kind === 'module'; + } +} + +function format_table(f, data, depth = 0) { + if (!data.length) { + return; + } + + const column_sizes = new Array(data[0].length).fill(0); + + data.forEach((row) => { + row.forEach((e, idx) => { + column_sizes[idx] = Math.max(e.length, column_sizes[idx]); + }); + }); + + const indent = ' '.repeat(depth); + let sep = indent; + column_sizes.forEach((size) => { + sep += '+'; + sep += '-'.repeat(size + 2); + }); + sep += '+\n'; + f.write(sep); + + data.forEach((row) => { + let row_text = `${indent}|`; + row.forEach((entry, idx) => { + row_text += ` ${entry.padEnd(column_sizes[idx])} |`; + }); + row_text += '\n'; + f.write(row_text); + f.write(sep); + }); + + f.write('\n'); +} + +function make_header(header, sep) { + return `${header}\n${sep.repeat(header.length)}\n\n`; +} + +function indent_multiline(text, depth) { + const indent = ' '.repeat(depth); + return text.split('\n').map((l) => (l === '' ? l : indent + l)).join('\n'); +} + +function make_rst_signature(obj, types = false, style = false) { + let out = ''; + const fmt = style ? '*' : ''; + obj.params.forEach((arg, idx) => { + if (idx > 0) { + if (arg.optional) { + out += ` ${fmt}[`; + } + out += ', '; + } else { + out += ' '; + if (arg.optional) { + out += `${fmt}[ `; + } + } + if (types) { + out += `${arg.type} `; + } + const variable = arg.variable ? '...' : ''; + const defval = arg.defaultvalue !== undefined ? `=${arg.defaultvalue}` : ''; + out += `${variable}${arg.name}${defval}`; + if (arg.optional) { + out += ` ]${fmt}`; + } + }); + out += ' '; + return out; +} + +function make_rst_param(f, obj, depth = 0) { + const indent = ' '.repeat(depth * 3); + f.write(indent); + f.write(`:param ${obj.type} ${obj.name}:\n`); + f.write(indent_multiline(obj.description, (depth + 1) * 3)); + f.write('\n\n'); +} + +function make_rst_attribute(f, obj, depth = 0, brief = false) { + const indent = ' '.repeat(depth * 3); + f.write(indent); + f.write(`.. js:attribute:: ${obj.name}\n\n`); + + if (brief) { + if (obj.summary) { + f.write(indent_multiline(obj.summary, (depth + 1) * 3)); + } + f.write('\n\n'); + return; + } + + f.write(indent_multiline(obj.description, (depth + 1) * 3)); + f.write('\n\n'); + + f.write(indent); + f.write(` :type: ${obj.type}\n\n`); + + if (obj.defaultvalue !== undefined) { + let defval = obj.defaultvalue; + if (defval === '') { + defval = '""'; + } + f.write(indent); + f.write(` :value: \`\`${defval}\`\`\n\n`); + } +} + +function make_rst_function(f, obj, depth = 0) { + let prefix = ''; + if (obj.is_instance()) { + prefix = 'prototype.'; + } + + const indent = ' '.repeat(depth * 3); + const sig = make_rst_signature(obj); + f.write(indent); + f.write(`.. js:function:: ${prefix}${obj.name}(${sig})\n`); + f.write('\n'); + + f.write(indent_multiline(obj.description, (depth + 1) * 3)); + f.write('\n\n'); + + obj.params.forEach((param) => { + make_rst_param(f, param, depth + 1); + }); + + if (obj.returns !== 'void') { + f.write(indent); + f.write(' :return:\n'); + f.write(indent_multiline(obj.returns_desc, (depth + 2) * 3)); + f.write('\n\n'); + f.write(indent); + f.write(` :rtype: ${obj.returns}\n\n`); + } +} + +function make_rst_object(f, obj) { + let brief = false; + // Our custom header flag. + if (obj.header !== null) { + f.write(make_header(obj.header, '-')); + f.write(`${obj.description}\n\n`); + brief = true; + } + + // Format members table and descriptions + const data = [['type', 'name']].concat(obj.members.map((m) => [m.type, `:js:attr:\`${m.name}\``])); + + f.write(make_header('Properties', '^')); + format_table(f, data, 0); + + make_rst_attribute(f, obj, 0, brief); + + if (!obj.members.length) { + return; + } + + f.write(' **Property Descriptions**\n\n'); + + // Properties first + obj.members.filter((m) => !m.is_function()).forEach((m) => { + make_rst_attribute(f, m, 1); + }); + + // Callbacks last + obj.members.filter((m) => m.is_function()).forEach((m) => { + make_rst_function(f, m, 1); + }); +} + +function make_rst_class(f, obj) { + const header = obj.header ? obj.header : obj.name; + f.write(make_header(header, '-')); + + if (obj.classdesc) { + f.write(`${obj.classdesc}\n\n`); + } + + const funcs = obj.members.filter((m) => m.is_function()); + function make_data(m) { + const base = m.is_static() ? obj.name : `${obj.name}.prototype`; + const params = make_rst_signature(m, true, true); + const sig = `:js:attr:\`${m.name} <${base}.${m.name}>\` **(**${params}**)**`; + return [m.returns, sig]; + } + const sfuncs = funcs.filter((m) => m.is_static()); + const ifuncs = funcs.filter((m) => !m.is_static()); + + f.write(make_header('Static Methods', '^')); + format_table(f, sfuncs.map((m) => make_data(m))); + + f.write(make_header('Instance Methods', '^')); + format_table(f, ifuncs.map((m) => make_data(m))); + + const sig = make_rst_signature(obj); + f.write(`.. js:class:: ${obj.name}(${sig})\n\n`); + f.write(indent_multiline(obj.description, 3)); + f.write('\n\n'); + + obj.params.forEach((p) => { + make_rst_param(f, p, 1); + }); + + f.write(' **Static Methods**\n\n'); + sfuncs.forEach((m) => { + make_rst_function(f, m, 1); + }); + + f.write(' **Instance Methods**\n\n'); + ifuncs.forEach((m) => { + make_rst_function(f, m, 1); + }); +} + +function make_rst_module(f, obj) { + const header = obj.header !== null ? obj.header : obj.name; + f.write(make_header(header, '=')); + f.write(obj.description); + f.write('\n\n'); +} + +function write_base_object(f, obj) { + if (obj.is_object()) { + make_rst_object(f, obj); + } else if (obj.is_function()) { + make_rst_function(f, obj); + } else if (obj.is_class()) { + make_rst_class(f, obj); + } else if (obj.is_module()) { + make_rst_module(f, obj); + } +} + +function generate(f, docs) { + const globs = []; + const SYMBOLS = {}; + docs.filter((d) => !d.ignore && d.kind !== 'package').forEach((d) => { + SYMBOLS[d.name] = d; + if (d.memberof) { + const up = SYMBOLS[d.memberof]; + if (up === undefined) { + console.log(d); // eslint-disable-line no-console + console.log(`Undefined symbol! ${d.memberof}`); // eslint-disable-line no-console + throw new Error('Undefined symbol!'); + } + SYMBOLS[d.memberof].add_member(d); + } else { + globs.push(d); + } + }); + + f.write('.. _doc_html5_shell_classref:\n\n'); + globs.forEach((obj) => write_base_object(f, obj)); +} + +/** + * Generate documentation output. + * + * @param {TAFFY} data - A TaffyDB collection representing + * all the symbols documented in your code. + * @param {object} opts - An object with options information. + */ +exports.publish = function (data, opts) { + const docs = data().get().filter((doc) => !doc.undocumented && !doc.ignore).map((doc) => new JSDoclet(doc)); + const dest = opts.destination; + if (dest === 'dry-run') { + process.stdout.write('Dry run... '); + generate({ + write: function () { /* noop */ }, + }, docs); + process.stdout.write('Okay!\n'); + return; + } + if (dest !== '' && !dest.endsWith('.rst')) { + throw new Error('Destination file must be either a ".rst" file, or an empty string (for printing to stdout)'); + } + if (dest !== '') { + const f = fs.createWriteStream(dest); + generate(f, docs); + } else { + generate(process.stdout, docs); + } +}; diff --git a/platform/javascript/js/libs/library_godot_audio.js b/platform/javascript/js/libs/library_godot_audio.js index 8e385e9176..ac4055516c 100644 --- a/platform/javascript/js/libs/library_godot_audio.js +++ b/platform/javascript/js/libs/library_godot_audio.js @@ -238,6 +238,9 @@ const GodotAudioWorklet = { close: function () { return new Promise(function (resolve, reject) { + if (GodotAudioWorklet.promise === null) { + return; + } GodotAudioWorklet.promise.then(function () { GodotAudioWorklet.worklet.port.postMessage({ 'cmd': 'stop', @@ -247,7 +250,7 @@ const GodotAudioWorklet = { GodotAudioWorklet.worklet = null; GodotAudioWorklet.promise = null; resolve(); - }); + }).catch(function (err) { /* aborted? */ }); }); }, }, diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js index 9ec295b39f..99aa4793d9 100644 --- a/platform/javascript/js/libs/library_godot_display.js +++ b/platform/javascript/js/libs/library_godot_display.js @@ -192,33 +192,45 @@ const GodotDisplayDragDrop = { GodotDisplayDragDrop.promises = []; GodotDisplayDragDrop.pending_files = []; callback(drops); - const dirs = [DROP.substr(0, DROP.length - 1)]; - // Remove temporary files - files.forEach(function (file) { - FS.unlink(file); - let dir = file.replace(DROP, ''); - let idx = dir.lastIndexOf('/'); - while (idx > 0) { - dir = dir.substr(0, idx); - if (dirs.indexOf(DROP + dir) === -1) { - dirs.push(DROP + dir); - } - idx = dir.lastIndexOf('/'); - } - }); - // Remove dirs. - dirs.sort(function (a, b) { - const al = (a.match(/\//g) || []).length; - const bl = (b.match(/\//g) || []).length; - if (al > bl) { - return -1; - } else if (al < bl) { - return 1; + if (GodotConfig.persistent_drops) { + // Delay removal at exit. + GodotOS.atexit(function (resolve, reject) { + GodotDisplayDragDrop.remove_drop(files, DROP); + resolve(); + }); + } else { + GodotDisplayDragDrop.remove_drop(files, DROP); + } + }); + }, + + remove_drop: function (files, drop_path) { + const dirs = [drop_path.substr(0, drop_path.length - 1)]; + // Remove temporary files + files.forEach(function (file) { + FS.unlink(file); + let dir = file.replace(drop_path, ''); + let idx = dir.lastIndexOf('/'); + while (idx > 0) { + dir = dir.substr(0, idx); + if (dirs.indexOf(drop_path + dir) === -1) { + dirs.push(drop_path + dir); } - return 0; - }).forEach(function (dir) { - FS.rmdir(dir); - }); + idx = dir.lastIndexOf('/'); + } + }); + // Remove dirs. + dirs.sort(function (a, b) { + const al = (a.match(/\//g) || []).length; + const bl = (b.match(/\//g) || []).length; + if (al > bl) { + return -1; + } else if (al < bl) { + return 1; + } + return 0; + }).forEach(function (dir) { + FS.rmdir(dir); }); }, @@ -231,6 +243,105 @@ const GodotDisplayDragDrop = { }; mergeInto(LibraryManager.library, GodotDisplayDragDrop); +const GodotDisplayVK = { + + $GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotDisplayListeners'], + $GodotDisplayVK__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayVK.clear(); resolve(); });', + $GodotDisplayVK: { + textinput: null, + textarea: null, + + available: function () { + return GodotConfig.virtual_keyboard && 'ontouchstart' in window; + }, + + init: function (input_cb) { + function create(what) { + const elem = document.createElement(what); + elem.style.display = 'none'; + elem.style.position = 'absolute'; + elem.style.zIndex = '-1'; + elem.style.background = 'transparent'; + elem.style.padding = '0px'; + elem.style.margin = '0px'; + elem.style.overflow = 'hidden'; + elem.style.width = '0px'; + elem.style.height = '0px'; + elem.style.border = '0px'; + elem.style.outline = 'none'; + elem.readonly = true; + elem.disabled = true; + GodotDisplayListeners.add(elem, 'input', function (evt) { + const c_str = GodotRuntime.allocString(elem.value); + input_cb(c_str, elem.selectionEnd); + GodotRuntime.free(c_str); + }, false); + GodotDisplayListeners.add(elem, 'blur', function (evt) { + elem.style.display = 'none'; + elem.readonly = true; + elem.disabled = true; + }, false); + GodotConfig.canvas.insertAdjacentElement('beforebegin', elem); + return elem; + } + GodotDisplayVK.textinput = create('input'); + GodotDisplayVK.textarea = create('textarea'); + GodotDisplayVK.updateSize(); + }, + show: function (text, multiline, start, end) { + if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) { + return; + } + if (GodotDisplayVK.textinput.style.display !== '' || GodotDisplayVK.textarea.style.display !== '') { + GodotDisplayVK.hide(); + } + GodotDisplayVK.updateSize(); + const elem = multiline ? GodotDisplayVK.textarea : GodotDisplayVK.textinput; + elem.readonly = false; + elem.disabled = false; + elem.value = text; + elem.style.display = 'block'; + elem.focus(); + elem.setSelectionRange(start, end); + }, + hide: function () { + if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) { + return; + } + [GodotDisplayVK.textinput, GodotDisplayVK.textarea].forEach(function (elem) { + elem.blur(); + elem.style.display = 'none'; + elem.value = ''; + }); + }, + updateSize: function () { + if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) { + return; + } + const rect = GodotConfig.canvas.getBoundingClientRect(); + function update(elem) { + elem.style.left = `${rect.left}px`; + elem.style.top = `${rect.top}px`; + elem.style.width = `${rect.width}px`; + elem.style.height = `${rect.height}px`; + } + update(GodotDisplayVK.textinput); + update(GodotDisplayVK.textarea); + }, + clear: function () { + if (GodotDisplayVK.textinput) { + GodotDisplayVK.textinput.remove(); + GodotDisplayVK.textinput = null; + } + if (GodotDisplayVK.textarea) { + GodotDisplayVK.textarea.remove(); + GodotDisplayVK.textarea = null; + } + }, + }, +}; +mergeInto(LibraryManager.library, GodotDisplayVK); + /* * Display server cursor helper. * Keeps track of cursor status and custom shapes. @@ -396,13 +507,122 @@ const GodotDisplayGamepads = { }; mergeInto(LibraryManager.library, GodotDisplayGamepads); +const GodotDisplayScreen = { + $GodotDisplayScreen__deps: ['$GodotConfig', '$GodotOS', '$GL', 'emscripten_webgl_get_current_context'], + $GodotDisplayScreen: { + desired_size: [0, 0], + hidpi: true, + getPixelRatio: function () { + return GodotDisplayScreen.hidpi ? window.devicePixelRatio || 1 : 1; + }, + isFullscreen: function () { + const elem = document.fullscreenElement || document.mozFullscreenElement + || document.webkitFullscreenElement || document.msFullscreenElement; + if (elem) { + return elem === GodotConfig.canvas; + } + // But maybe knowing the element is not supported. + return document.fullscreen || document.mozFullScreen + || document.webkitIsFullscreen; + }, + hasFullscreen: function () { + return document.fullscreenEnabled || document.mozFullScreenEnabled + || document.webkitFullscreenEnabled; + }, + requestFullscreen: function () { + if (!GodotDisplayScreen.hasFullscreen()) { + return 1; + } + const canvas = GodotConfig.canvas; + try { + const promise = (canvas.requestFullscreen || canvas.msRequestFullscreen + || canvas.mozRequestFullScreen || canvas.mozRequestFullscreen + || canvas.webkitRequestFullscreen + ).call(canvas); + // Some browsers (Safari) return undefined. + // For the standard ones, we need to catch it. + if (promise) { + promise.catch(function () { + // nothing to do. + }); + } + } catch (e) { + return 1; + } + return 0; + }, + exitFullscreen: function () { + if (!GodotDisplayScreen.isFullscreen()) { + return 0; + } + try { + const promise = document.exitFullscreen(); + if (promise) { + promise.catch(function () { + // nothing to do. + }); + } + } catch (e) { + return 1; + } + return 0; + }, + _updateGL: function () { + const gl_context_handle = _emscripten_webgl_get_current_context(); // eslint-disable-line no-undef + const gl = GL.getContext(gl_context_handle); + if (gl) { + GL.resizeOffscreenFramebuffer(gl); + } + }, + updateSize: function () { + const isFullscreen = GodotDisplayScreen.isFullscreen(); + const wantsFullWindow = GodotConfig.canvas_resize_policy === 2; + const noResize = GodotConfig.canvas_resize_policy === 0; + const wwidth = GodotDisplayScreen.desired_size[0]; + const wheight = GodotDisplayScreen.desired_size[1]; + const canvas = GodotConfig.canvas; + let width = wwidth; + let height = wheight; + if (noResize) { + // Don't resize canvas, just update GL if needed. + if (canvas.width !== width || canvas.height !== height) { + GodotDisplayScreen.desired_size = [canvas.width, canvas.height]; + GodotDisplayScreen._updateGL(); + return 1; + } + return 0; + } + const scale = GodotDisplayScreen.getPixelRatio(); + if (isFullscreen || wantsFullWindow) { + // We need to match screen size. + width = window.innerWidth * scale; + height = window.innerHeight * scale; + } + const csw = `${width / scale}px`; + const csh = `${height / scale}px`; + if (canvas.style.width !== csw || canvas.style.height !== csh || canvas.width !== width || canvas.height !== height) { + // Size doesn't match. + // Resize canvas, set correct CSS pixel size, update GL. + canvas.width = width; + canvas.height = height; + canvas.style.width = csw; + canvas.style.height = csh; + GodotDisplayScreen._updateGL(); + return 1; + } + return 0; + }, + }, +}; +mergeInto(LibraryManager.library, GodotDisplayScreen); + /** * Display server interface. * * Exposes all the functions needed by DisplayServer implementation. */ const GodotDisplay = { - $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop', '$GodotDisplayGamepads'], + $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop', '$GodotDisplayGamepads', '$GodotDisplayScreen', '$GodotDisplayVK'], $GodotDisplay: { window_icon: '', findDPI: function () { @@ -450,7 +670,53 @@ const GodotDisplay = { godot_js_display_pixel_ratio_get__sig: 'f', godot_js_display_pixel_ratio_get: function () { - return window.devicePixelRatio || 1; + return GodotDisplayScreen.getPixelRatio(); + }, + + godot_js_display_fullscreen_request__sig: 'i', + godot_js_display_fullscreen_request: function () { + return GodotDisplayScreen.requestFullscreen(); + }, + + godot_js_display_fullscreen_exit__sig: 'i', + godot_js_display_fullscreen_exit: function () { + return GodotDisplayScreen.exitFullscreen(); + }, + + godot_js_display_desired_size_set__sig: 'v', + godot_js_display_desired_size_set: function (width, height) { + GodotDisplayScreen.desired_size = [width, height]; + GodotDisplayScreen.updateSize(); + }, + + godot_js_display_size_update__sig: 'i', + godot_js_display_size_update: function () { + const updated = GodotDisplayScreen.updateSize(); + if (updated) { + GodotDisplayVK.updateSize(); + } + return updated; + }, + + godot_js_display_screen_size_get__sig: 'vii', + godot_js_display_screen_size_get: function (width, height) { + const scale = GodotDisplayScreen.getPixelRatio(); + GodotRuntime.setHeapValue(width, window.screen.width * scale, 'i32'); + GodotRuntime.setHeapValue(height, window.screen.height * scale, 'i32'); + }, + + godot_js_display_window_size_get: function (p_width, p_height) { + GodotRuntime.setHeapValue(p_width, GodotConfig.canvas.width, 'i32'); + GodotRuntime.setHeapValue(p_height, GodotConfig.canvas.height, 'i32'); + }, + + godot_js_display_compute_position: function (x, y, r_x, r_y) { + const canvas = GodotConfig.canvas; + const rect = canvas.getBoundingClientRect(); + const rw = canvas.width / rect.width; + const rh = canvas.height / rect.height; + GodotRuntime.setHeapValue(r_x, (x - rect.x) * rw, 'i32'); + GodotRuntime.setHeapValue(r_y, (y - rect.y) * rh, 'i32'); }, /* @@ -466,13 +732,6 @@ const GodotDisplay = { return document.activeElement === GodotConfig.canvas; }, - godot_js_display_canvas_bounding_rect_position_get__sig: 'vii', - godot_js_display_canvas_bounding_rect_position_get: function (r_x, r_y) { - const brect = GodotConfig.canvas.getBoundingClientRect(); - GodotRuntime.setHeapValue(r_x, brect.x, 'i32'); - GodotRuntime.setHeapValue(r_y, brect.y, 'i32'); - }, - /* * Touchscreen */ @@ -516,15 +775,6 @@ const GodotDisplay = { /* * Window */ - godot_js_display_window_request_fullscreen__sig: 'v', - godot_js_display_window_request_fullscreen: function () { - const canvas = GodotConfig.canvas; - (canvas.requestFullscreen || canvas.msRequestFullscreen - || canvas.mozRequestFullScreen || canvas.mozRequestFullscreen - || canvas.webkitRequestFullscreen - ).call(canvas); - }, - godot_js_display_window_title_set__sig: 'vi', godot_js_display_window_title_set: function (p_data) { document.title = GodotRuntime.parseString(p_data); @@ -540,7 +790,7 @@ const GodotDisplay = { document.head.appendChild(link); } const old_icon = GodotDisplay.window_icon; - const png = new Blob([GodotRuntime.heapCopy(HEAPU8, p_ptr, p_len)], { type: 'image/png' }); + const png = new Blob([GodotRuntime.heapSlice(HEAPU8, p_ptr, p_len)], { type: 'image/png' }); GodotDisplay.window_icon = URL.createObjectURL(png); link.href = GodotDisplay.window_icon; if (old_icon) { @@ -580,7 +830,7 @@ const GodotDisplay = { const shape = GodotRuntime.parseString(p_shape); const old_shape = GodotDisplayCursor.cursors[shape]; if (p_len > 0) { - const png = new Blob([GodotRuntime.heapCopy(HEAPU8, p_ptr, p_len)], { type: 'image/png' }); + const png = new Blob([GodotRuntime.heapSlice(HEAPU8, p_ptr, p_len)], { type: 'image/png' }); const url = URL.createObjectURL(png); GodotDisplayCursor.cursors[shape] = { url: url, @@ -645,8 +895,8 @@ const GodotDisplay = { GodotDisplayListeners.add(canvas, 'drop', GodotDisplayDragDrop.handler(dropFiles)); }, - godot_js_display_setup_canvas__sig: 'v', - godot_js_display_setup_canvas: function () { + godot_js_display_setup_canvas__sig: 'viiii', + godot_js_display_setup_canvas: function (p_width, p_height, p_fullscreen, p_hidpi) { const canvas = GodotConfig.canvas; GodotDisplayListeners.add(canvas, 'contextmenu', function (ev) { ev.preventDefault(); @@ -655,6 +905,54 @@ const GodotDisplay = { alert('WebGL context lost, please reload the page'); // eslint-disable-line no-alert ev.preventDefault(); }, false); + GodotDisplayScreen.hidpi = !!p_hidpi; + switch (GodotConfig.canvas_resize_policy) { + case 0: // None + GodotDisplayScreen.desired_size = [canvas.width, canvas.height]; + break; + case 1: // Project + GodotDisplayScreen.desired_size = [p_width, p_height]; + break; + default: // Full window + // Ensure we display in the right place, the size will be handled by updateSize + canvas.style.position = 'absolute'; + canvas.style.top = 0; + canvas.style.left = 0; + break; + } + GodotDisplayScreen.updateSize(); + if (p_fullscreen) { + GodotDisplayScreen.requestFullscreen(); + } + }, + + /* + * Virtual Keyboard + */ + godot_js_display_vk_show__sig: 'viiii', + godot_js_display_vk_show: function (p_text, p_multiline, p_start, p_end) { + const text = GodotRuntime.parseString(p_text); + const start = p_start > 0 ? p_start : 0; + const end = p_end > 0 ? p_end : start; + GodotDisplayVK.show(text, p_multiline, start, end); + }, + + godot_js_display_vk_hide__sig: 'v', + godot_js_display_vk_hide: function () { + GodotDisplayVK.hide(); + }, + + godot_js_display_vk_available__sig: 'i', + godot_js_display_vk_available: function () { + return GodotDisplayVK.available(); + }, + + godot_js_display_vk_cb__sig: 'vi', + godot_js_display_vk_cb: function (p_input_cb) { + const input_cb = GodotRuntime.get_func(p_input_cb); + if (GodotDisplayVK.available()) { + GodotDisplayVK.init(input_cb); + } }, /* diff --git a/platform/javascript/js/libs/library_godot_fetch.js b/platform/javascript/js/libs/library_godot_fetch.js new file mode 100644 index 0000000000..4ae6a23593 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_fetch.js @@ -0,0 +1,258 @@ +/*************************************************************************/ +/* library_godot_fetch.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +const GodotFetch = { + $GodotFetch__deps: ['$GodotRuntime'], + $GodotFetch: { + + onread: function (id, result) { + const obj = IDHandler.get(id); + if (!obj) { + return; + } + if (result.value) { + obj.chunks.push(result.value); + } + obj.reading = false; + obj.done = result.done; + }, + + onresponse: function (id, response) { + const obj = IDHandler.get(id); + if (!obj) { + return; + } + let size = -1; + let compressed = false; + let chunked = false; + response.headers.forEach(function (value, header) { + const v = value.toLowerCase().trim(); + const h = header.toLowerCase().trim(); + if (h === 'content-encoding') { + compressed = true; + size = -1; + } else if (h === 'content-length') { + const len = Number.parseInt(value, 10); + if (!Number.isNaN(len) && !compressed) { + size = len; + } + } else if (h === 'transfer-encoding' && v === 'chunked') { + chunked = true; + } + }); + obj.bodySize = size; + obj.status = response.status; + obj.response = response; + obj.reader = response.body.getReader(); + obj.chunked = chunked; + }, + + onerror: function (id, err) { + GodotRuntime.error(err); + const obj = IDHandler.get(id); + if (!obj) { + return; + } + obj.error = err; + }, + + create: function (method, url, headers, body) { + const obj = { + request: null, + response: null, + reader: null, + error: null, + done: false, + reading: false, + status: 0, + chunks: [], + bodySize: -1, + }; + const id = IDHandler.add(obj); + const init = { + method: method, + headers: headers, + body: body, + }; + obj.request = fetch(url, init); + obj.request.then(GodotFetch.onresponse.bind(null, id)).catch(GodotFetch.onerror.bind(null, id)); + return id; + }, + + free: function (id) { + const obj = IDHandler.get(id); + if (!obj) { + return; + } + IDHandler.remove(id); + if (!obj.request) { + return; + } + // Try to abort + obj.request.then(function (response) { + response.abort(); + }).catch(function (e) { /* nothing to do */ }); + }, + + read: function (id) { + const obj = IDHandler.get(id); + if (!obj) { + return; + } + if (obj.reader && !obj.reading) { + if (obj.done) { + obj.reader = null; + return; + } + obj.reading = true; + obj.reader.read().then(GodotFetch.onread.bind(null, id)).catch(GodotFetch.onerror.bind(null, id)); + } + }, + }, + + godot_js_fetch_create__sig: 'iii', + godot_js_fetch_create: function (p_method, p_url, p_headers, p_headers_size, p_body, p_body_size) { + const method = GodotRuntime.parseString(p_method); + const url = GodotRuntime.parseString(p_url); + const headers = GodotRuntime.parseStringArray(p_headers, p_headers_size); + const body = p_body_size ? GodotRuntime.heapSlice(HEAP8, p_body, p_body_size) : null; + return GodotFetch.create(method, url, headers.map(function (hv) { + const idx = hv.indexOf(':'); + if (idx <= 0) { + return []; + } + return [ + hv.slice(0, idx).trim(), + hv.slice(idx + 1).trim(), + ]; + }).filter(function (v) { + return v.length === 2; + }), body); + }, + + godot_js_fetch_state_get__sig: 'ii', + godot_js_fetch_state_get: function (p_id) { + const obj = IDHandler.get(p_id); + if (!obj) { + return -1; + } + if (obj.error) { + return -1; + } + if (!obj.response) { + return 0; + } + if (obj.reader) { + return 1; + } + if (obj.done) { + return 2; + } + return -1; + }, + + godot_js_fetch_http_status_get__sig: 'ii', + godot_js_fetch_http_status_get: function (p_id) { + const obj = IDHandler.get(p_id); + if (!obj || !obj.response) { + return 0; + } + return obj.status; + }, + + godot_js_fetch_read_headers__sig: 'iii', + godot_js_fetch_read_headers: function (p_id, p_parse_cb, p_ref) { + const obj = IDHandler.get(p_id); + if (!obj || !obj.response) { + return 1; + } + const cb = GodotRuntime.get_func(p_parse_cb); + const arr = []; + obj.response.headers.forEach(function (v, h) { + arr.push(`${h}:${v}`); + }); + const c_ptr = GodotRuntime.allocStringArray(arr); + cb(arr.length, c_ptr, p_ref); + GodotRuntime.freeStringArray(c_ptr, arr.length); + return 0; + }, + + godot_js_fetch_read_chunk__sig: 'ii', + godot_js_fetch_read_chunk: function (p_id, p_buf, p_buf_size) { + const obj = IDHandler.get(p_id); + if (!obj || !obj.response) { + return 0; + } + let to_read = p_buf_size; + const chunks = obj.chunks; + while (to_read && chunks.length) { + const chunk = obj.chunks[0]; + if (chunk.length > to_read) { + GodotRuntime.heapCopy(HEAP8, chunk.slice(0, to_read), p_buf); + chunks[0] = chunk.slice(to_read); + to_read = 0; + } else { + GodotRuntime.heapCopy(HEAP8, chunk, p_buf); + to_read -= chunk.length; + chunks.pop(); + } + } + if (!chunks.length) { + GodotFetch.read(p_id); + } + return p_buf_size - to_read; + }, + + godot_js_fetch_body_length_get__sig: 'ii', + godot_js_fetch_body_length_get: function (p_id) { + const obj = IDHandler.get(p_id); + if (!obj || !obj.response) { + return -1; + } + return obj.bodySize; + }, + + godot_js_fetch_is_chunked__sig: 'ii', + godot_js_fetch_is_chunked: function (p_id) { + const obj = IDHandler.get(p_id); + if (!obj || !obj.response) { + return -1; + } + return obj.chunked ? 1 : 0; + }, + + godot_js_fetch_free__sig: 'vi', + godot_js_fetch_free: function (id) { + GodotFetch.free(id); + }, +}; + +autoAddDeps(GodotFetch, '$GodotFetch'); +mergeInto(LibraryManager.library, GodotFetch); diff --git a/platform/javascript/js/libs/library_godot_http_request.js b/platform/javascript/js/libs/library_godot_http_request.js deleted file mode 100644 index 7dc2a2df29..0000000000 --- a/platform/javascript/js/libs/library_godot_http_request.js +++ /dev/null @@ -1,142 +0,0 @@ -/*************************************************************************/ -/* http_request.js */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/*************************************************************************/ - -const GodotHTTPRequest = { - $GodotHTTPRequest__deps: ['$GodotRuntime'], - $GodotHTTPRequest: { - requests: [], - - getUnusedRequestId: function () { - const idMax = GodotHTTPRequest.requests.length; - for (let potentialId = 0; potentialId < idMax; ++potentialId) { - if (GodotHTTPRequest.requests[potentialId] instanceof XMLHttpRequest) { - continue; - } - return potentialId; - } - GodotHTTPRequest.requests.push(null); - return idMax; - }, - - setupRequest: function (xhr) { - xhr.responseType = 'arraybuffer'; - }, - }, - - godot_xhr_new__sig: 'i', - godot_xhr_new: function () { - const newId = GodotHTTPRequest.getUnusedRequestId(); - GodotHTTPRequest.requests[newId] = new XMLHttpRequest(); - GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[newId]); - return newId; - }, - - godot_xhr_reset__sig: 'vi', - godot_xhr_reset: function (xhrId) { - GodotHTTPRequest.requests[xhrId] = new XMLHttpRequest(); - GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[xhrId]); - }, - - godot_xhr_free__sig: 'vi', - godot_xhr_free: function (xhrId) { - GodotHTTPRequest.requests[xhrId].abort(); - GodotHTTPRequest.requests[xhrId] = null; - }, - - godot_xhr_open__sig: 'viiiii', - godot_xhr_open: function (xhrId, method, url, p_user, p_password) { - const user = p_user > 0 ? GodotRuntime.parseString(p_user) : null; - const password = p_password > 0 ? GodotRuntime.parseString(p_password) : null; - GodotHTTPRequest.requests[xhrId].open(GodotRuntime.parseString(method), GodotRuntime.parseString(url), true, user, password); - }, - - godot_xhr_set_request_header__sig: 'viii', - godot_xhr_set_request_header: function (xhrId, header, value) { - GodotHTTPRequest.requests[xhrId].setRequestHeader(GodotRuntime.parseString(header), GodotRuntime.parseString(value)); - }, - - godot_xhr_send__sig: 'viii', - godot_xhr_send: function (xhrId, p_ptr, p_len) { - let data = null; - if (p_ptr && p_len) { - data = GodotRuntime.heapCopy(HEAP8, p_ptr, p_len); - } - GodotHTTPRequest.requests[xhrId].send(data); - }, - - godot_xhr_abort__sig: 'vi', - godot_xhr_abort: function (xhrId) { - GodotHTTPRequest.requests[xhrId].abort(); - }, - - godot_xhr_get_status__sig: 'ii', - godot_xhr_get_status: function (xhrId) { - return GodotHTTPRequest.requests[xhrId].status; - }, - - godot_xhr_get_ready_state__sig: 'ii', - godot_xhr_get_ready_state: function (xhrId) { - return GodotHTTPRequest.requests[xhrId].readyState; - }, - - godot_xhr_get_response_headers_length__sig: 'ii', - godot_xhr_get_response_headers_length: function (xhrId) { - const headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); - return headers === null ? 0 : GodotRuntime.strlen(headers); - }, - - godot_xhr_get_response_headers__sig: 'viii', - godot_xhr_get_response_headers: function (xhrId, dst, len) { - const str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); - if (str === null) { - return; - } - GodotRuntime.stringToHeap(str, dst, len); - }, - - godot_xhr_get_response_length__sig: 'ii', - godot_xhr_get_response_length: function (xhrId) { - const body = GodotHTTPRequest.requests[xhrId].response; - return body === null ? 0 : body.byteLength; - }, - - godot_xhr_get_response__sig: 'viii', - godot_xhr_get_response: function (xhrId, dst, len) { - let buf = GodotHTTPRequest.requests[xhrId].response; - if (buf === null) { - return; - } - buf = new Uint8Array(buf).subarray(0, len); - HEAPU8.set(buf, dst); - }, -}; - -autoAddDeps(GodotHTTPRequest, '$GodotHTTPRequest'); -mergeInto(LibraryManager.library, GodotHTTPRequest); diff --git a/platform/javascript/js/libs/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js index 9fde4a84e1..1d9f889bce 100644 --- a/platform/javascript/js/libs/library_godot_os.js +++ b/platform/javascript/js/libs/library_godot_os.js @@ -58,21 +58,34 @@ const GodotConfig = { $GodotConfig: { canvas: null, locale: 'en', - resize_on_start: false, + canvas_resize_policy: 2, // Adaptive + virtual_keyboard: false, + persistent_drops: false, on_execute: null, + on_exit: null, init_config: function (p_opts) { - GodotConfig.resize_on_start = !!p_opts['resizeCanvasOnStart']; + GodotConfig.canvas_resize_policy = p_opts['canvasResizePolicy']; GodotConfig.canvas = p_opts['canvas']; GodotConfig.locale = p_opts['locale'] || GodotConfig.locale; + GodotConfig.virtual_keyboard = p_opts['virtualKeyboard']; + GodotConfig.persistent_drops = !!p_opts['persistentDrops']; GodotConfig.on_execute = p_opts['onExecute']; - // This is called by emscripten, even if undocumented. - Module['onExit'] = p_opts['onExit']; // eslint-disable-line no-undef + GodotConfig.on_exit = p_opts['onExit']; }, locate_file: function (file) { return Module['locateFile'](file); // eslint-disable-line no-undef }, + clear: function () { + GodotConfig.canvas = null; + GodotConfig.locale = 'en'; + GodotConfig.canvas_resize_policy = 2; + GodotConfig.virtual_keyboard = false; + GodotConfig.persistent_drops = false; + GodotConfig.on_execute = null; + GodotConfig.on_exit = null; + }, }, godot_js_config_canvas_id_get__sig: 'vii', @@ -84,11 +97,6 @@ const GodotConfig = { godot_js_config_locale_get: function (p_ptr, p_ptr_max) { GodotRuntime.stringToHeap(GodotConfig.locale, p_ptr, p_ptr_max); }, - - godot_js_config_is_resize_on_start__sig: 'i', - godot_js_config_is_resize_on_start: function () { - return GodotConfig.resize_on_start ? 1 : 0; - }, }; autoAddDeps(GodotConfig, '$GodotConfig'); @@ -98,7 +106,6 @@ const GodotFS = { $GodotFS__deps: ['$FS', '$IDBFS', '$GodotRuntime'], $GodotFS__postset: [ 'Module["initFS"] = GodotFS.init;', - 'Module["deinitFS"] = GodotFS.deinit;', 'Module["copyToFS"] = GodotFS.copy_to_fs;', ].join(''), $GodotFS: { @@ -210,9 +217,10 @@ const GodotFS = { mergeInto(LibraryManager.library, GodotFS); const GodotOS = { - $GodotOS__deps: ['$GodotFS', '$GodotRuntime'], + $GodotOS__deps: ['$GodotRuntime', '$GodotConfig', '$GodotFS'], $GodotOS__postset: [ 'Module["request_quit"] = function() { GodotOS.request_quit() };', + 'Module["onExit"] = GodotOS.cleanup;', 'GodotOS._fs_sync_promise = Promise.resolve();', ].join(''), $GodotOS: { @@ -224,6 +232,15 @@ const GodotOS = { GodotOS._async_cbs.push(p_promise_cb); }, + cleanup: function (exit_code) { + const cb = GodotConfig.on_exit; + GodotFS.deinit(); + GodotConfig.clear(); + if (cb) { + cb(exit_code); + } + }, + finish_async: function (callback) { GodotOS._fs_sync_promise.then(function (err) { const promises = []; @@ -282,6 +299,11 @@ const GodotOS = { godot_js_os_shell_open: function (p_uri) { window.open(GodotRuntime.parseString(p_uri), '_blank'); }, + + godot_js_os_hw_concurrency_get__sig: 'i', + godot_js_os_hw_concurrency_get: function () { + return navigator.hardwareConcurrency || 1; + }, }; autoAddDeps(GodotOS, '$GodotOS'); diff --git a/platform/javascript/js/libs/library_godot_runtime.js b/platform/javascript/js/libs/library_godot_runtime.js index 7e36ff8ab5..3da1ed8f06 100644 --- a/platform/javascript/js/libs/library_godot_runtime.js +++ b/platform/javascript/js/libs/library_godot_runtime.js @@ -72,11 +72,16 @@ const GodotRuntime = { return p_heap.subarray(p_ptr / bytes, p_ptr / bytes + p_len); }, - heapCopy: function (p_heap, p_ptr, p_len) { + heapSlice: function (p_heap, p_ptr, p_len) { const bytes = p_heap.BYTES_PER_ELEMENT; return p_heap.slice(p_ptr / bytes, p_ptr / bytes + p_len); }, + heapCopy: function (p_dst, p_src, p_ptr) { + const bytes = p_src.BYTES_PER_ELEMENT; + return p_dst.set(p_src, p_ptr / bytes); + }, + /* * Strings */ @@ -84,6 +89,15 @@ const GodotRuntime = { return UTF8ToString(p_ptr); // eslint-disable-line no-undef }, + parseStringArray: function (p_ptr, p_size) { + const strings = []; + const ptrs = GodotRuntime.heapSub(HEAP32, p_ptr, p_size); // TODO wasm64 + ptrs.forEach(function (ptr) { + strings.push(GodotRuntime.parseString(ptr)); + }); + return strings; + }, + strlen: function (p_str) { return lengthBytesUTF8(p_str); // eslint-disable-line no-undef }, |