diff options
Diffstat (limited to 'platform/web/js')
| -rw-r--r-- | platform/web/js/engine/config.js | 358 | ||||
| -rw-r--r-- | platform/web/js/engine/engine.externs.js | 4 | ||||
| -rw-r--r-- | platform/web/js/engine/engine.js | 281 | ||||
| -rw-r--r-- | platform/web/js/engine/preloader.js | 133 | ||||
| -rw-r--r-- | platform/web/js/jsdoc2rst/publish.js | 354 | ||||
| -rw-r--r-- | platform/web/js/libs/audio.worklet.js | 211 | ||||
| -rw-r--r-- | platform/web/js/libs/library_godot_audio.js | 484 | ||||
| -rw-r--r-- | platform/web/js/libs/library_godot_display.js | 754 | ||||
| -rw-r--r-- | platform/web/js/libs/library_godot_fetch.js | 247 | ||||
| -rw-r--r-- | platform/web/js/libs/library_godot_input.js | 549 | ||||
| -rw-r--r-- | platform/web/js/libs/library_godot_javascript_singleton.js | 346 | ||||
| -rw-r--r-- | platform/web/js/libs/library_godot_os.js | 427 | ||||
| -rw-r--r-- | platform/web/js/libs/library_godot_runtime.js | 134 | 
13 files changed, 4282 insertions, 0 deletions
diff --git a/platform/web/js/engine/config.js b/platform/web/js/engine/config.js new file mode 100644 index 0000000000..9c4b6c2012 --- /dev/null +++ b/platform/web/js/engine/config.js @@ -0,0 +1,358 @@ +/** + * 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, 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.<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 || {}; +		// 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.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]; +		} +		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); +}; diff --git a/platform/web/js/engine/engine.externs.js b/platform/web/js/engine/engine.externs.js new file mode 100644 index 0000000000..35a66a93ae --- /dev/null +++ b/platform/web/js/engine/engine.externs.js @@ -0,0 +1,4 @@ +var Godot; +var WebAssembly = {}; +WebAssembly.instantiate = function(buffer, imports) {}; +WebAssembly.instantiateStreaming = function(response, imports) {}; diff --git a/platform/web/js/engine/engine.js b/platform/web/js/engine/engine.js new file mode 100644 index 0000000000..6f0d51b2be --- /dev/null +++ b/platform/web/js/engine/engine.js @@ -0,0 +1,281 @@ +/** + * 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 Web export JavaScript reference + */ +const Engine = (function () { +	const preloader = new Preloader(); + +	let loadPromise = null; +	let loadPath = ''; +	let initPromise = null; + +	/** +	 * @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(`${loadPath}.wasm`, size, true); +			requestAnimationFrame(preloader.animateProgress); +		} +		return loadPromise; +	}; + +	/** +	 * 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; +	}; + +	/** +	 * 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; +	}; + +	/** +	 * 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) { +					// Care! Promise chaining is bogus with old emscripten versions. +					// This caused a regression with the Mono build (which uses an older emscripten version). +					// Make sure to test that when refactoring. +					return new Promise(function (resolve, reject) { +						promise.then(function (response) { +							const cloned = new Response(response.clone().body, { 'headers': [['content-type', 'application/wasm']] }); +							Godot(me.config.getModuleConfig(loadPath, cloned)).then(function (module) { +								const paths = me.config.persistentPaths; +								module['initFS'](paths).then(function (err) { +									me.rtenv = module; +									if (me.config.unloadAfterInit) { +										Engine.unload(); +									} +									resolve(); +								}); +							}); +						}); +					}); +				} +				preloader.setProgressFunc(this.config.onProgress); +				initPromise = doInit(loadPromise); +				return initPromise; +			}, + +			/** +			 * 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]); +			}, + +			/** +			 * 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')); +					} + +					let config = {}; +					try { +						config = me.config.getGodotConfig(function () { +							me.rtenv = null; +						}); +					} catch (e) { +						return Promise.reject(e); +					} +					// Godot configuration. +					me.rtenv['initConfig'](config); + +					// 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; +							if (me.config.serviceWorker && 'serviceWorker' in navigator) { +								navigator.serviceWorker.register(me.config.serviceWorker); +							} +							resolve(); +						}); +					}); +				}); +			}, + +			/** +			 * 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); +				}); +			}, + +			/** +			 * 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); +			}, + +			/** +			 * 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'](); +				} +			}, +		}; + +		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); +	} + +	// Closure compiler exported static methods. +	SafeEngine['load'] = Engine.load; +	SafeEngine['unload'] = Engine.unload; +	SafeEngine['isWebGLAvailable'] = Engine.isWebGLAvailable; + +	return SafeEngine; +}()); +if (typeof window !== 'undefined') { +	window['Engine'] = Engine; +} diff --git a/platform/web/js/engine/preloader.js b/platform/web/js/engine/preloader.js new file mode 100644 index 0000000000..564c68d264 --- /dev/null +++ b/platform/web/js/engine/preloader.js @@ -0,0 +1,133 @@ +const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars +	function getTrackedResponse(response, load_status) { +		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: fileSize || 0, +			loaded: 0, +			done: false, +		}; +		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) { +				return Promise.reject(err); +			} +			return new Promise(function (resolve, reject) { +				setTimeout(function () { +					retry(func, attempts - 1).then(resolve).catch(reject); +				}, 1000); +			}); +		} +		return func().catch(onerror); +	} + +	const DOWNLOAD_ATTEMPTS_MAX = 4; +	const loadingFiles = {}; +	const lastProgress = { loaded: 0, total: 0 }; +	let progressFunc = null; + +	const animateProgress = function () { +		let loaded = 0; +		let total = 0; +		let totalIsValid = true; +		let progressIsFinal = true; + +		Object.keys(loadingFiles).forEach(function (file) { +			const stat = loadingFiles[file]; +			if (!stat.done) { +				progressIsFinal = false; +			} +			if (!totalIsValid || stat.total === 0) { +				totalIsValid = false; +				total = 0; +			} else { +				total += stat.total; +			} +			loaded += stat.loaded; +		}); +		if (loaded !== lastProgress.loaded || total !== lastProgress.total) { +			lastProgress.loaded = loaded; +			lastProgress.total = total; +			if (typeof progressFunc === 'function') { +				progressFunc(loaded, total); +			} +		} +		if (!progressIsFinal) { +			requestAnimationFrame(animateProgress); +		} +	}; + +	this.animateProgress = animateProgress; + +	this.setProgressFunc = function (callback) { +		progressFunc = callback; +	}; + +	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, fileSize) { +		let buffer = null; +		if (typeof pathOrBuffer === 'string') { +			const me = this; +			return this.loadPromise(pathOrBuffer, fileSize).then(function (buf) { +				me.preloadedFiles.push({ +					path: destPath || pathOrBuffer, +					buffer: buf, +				}); +				return Promise.resolve(); +			}); +		} else if (pathOrBuffer instanceof ArrayBuffer) { +			buffer = new Uint8Array(pathOrBuffer); +		} else if (ArrayBuffer.isView(pathOrBuffer)) { +			buffer = new Uint8Array(pathOrBuffer.buffer); +		} +		if (buffer) { +			this.preloadedFiles.push({ +				path: destPath, +				buffer: pathOrBuffer, +			}); +			return Promise.resolve(); +		} +		return Promise.reject(new Error('Invalid object for preloading')); +	}; +}; diff --git a/platform/web/js/jsdoc2rst/publish.js b/platform/web/js/jsdoc2rst/publish.js new file mode 100644 index 0000000000..ad9c0fbaaa --- /dev/null +++ b/platform/web/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/web/js/libs/audio.worklet.js b/platform/web/js/libs/audio.worklet.js new file mode 100644 index 0000000000..ea4d8cb221 --- /dev/null +++ b/platform/web/js/libs/audio.worklet.js @@ -0,0 +1,211 @@ +/*************************************************************************/ +/*  audio.worklet.js                                                     */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2022 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.                */ +/*************************************************************************/ + +class RingBuffer { +	constructor(p_buffer, p_state, p_threads) { +		this.buffer = p_buffer; +		this.avail = p_state; +		this.threads = p_threads; +		this.rpos = 0; +		this.wpos = 0; +	} + +	data_left() { +		return this.threads ? Atomics.load(this.avail, 0) : this.avail; +	} + +	space_left() { +		return this.buffer.length - this.data_left(); +	} + +	read(output) { +		const size = this.buffer.length; +		let from = 0; +		let to_write = output.length; +		if (this.rpos + to_write > size) { +			const high = size - this.rpos; +			output.set(this.buffer.subarray(this.rpos, size)); +			from = high; +			to_write -= high; +			this.rpos = 0; +		} +		if (to_write) { +			output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from); +		} +		this.rpos += to_write; +		if (this.threads) { +			Atomics.add(this.avail, 0, -output.length); +			Atomics.notify(this.avail, 0); +		} else { +			this.avail -= output.length; +		} +	} + +	write(p_buffer) { +		const to_write = p_buffer.length; +		const mw = this.buffer.length - this.wpos; +		if (mw >= to_write) { +			this.buffer.set(p_buffer, this.wpos); +			this.wpos += to_write; +			if (mw === to_write) { +				this.wpos = 0; +			} +		} else { +			const high = p_buffer.subarray(0, mw); +			const low = p_buffer.subarray(mw); +			this.buffer.set(high, this.wpos); +			this.buffer.set(low); +			this.wpos = low.length; +		} +		if (this.threads) { +			Atomics.add(this.avail, 0, to_write); +			Atomics.notify(this.avail, 0); +		} else { +			this.avail += to_write; +		} +	} +} + +class GodotProcessor extends AudioWorkletProcessor { +	constructor() { +		super(); +		this.threads = false; +		this.running = true; +		this.lock = null; +		this.notifier = null; +		this.output = null; +		this.output_buffer = new Float32Array(); +		this.input = null; +		this.input_buffer = new Float32Array(); +		this.port.onmessage = (event) => { +			const cmd = event.data['cmd']; +			const data = event.data['data']; +			this.parse_message(cmd, data); +		}; +	} + +	process_notify() { +		if (this.notifier) { +			Atomics.add(this.notifier, 0, 1); +			Atomics.notify(this.notifier, 0); +		} +	} + +	parse_message(p_cmd, p_data) { +		if (p_cmd === 'start' && p_data) { +			const state = p_data[0]; +			let idx = 0; +			this.threads = true; +			this.lock = state.subarray(idx, ++idx); +			this.notifier = state.subarray(idx, ++idx); +			const avail_in = state.subarray(idx, ++idx); +			const avail_out = state.subarray(idx, ++idx); +			this.input = new RingBuffer(p_data[1], avail_in, true); +			this.output = new RingBuffer(p_data[2], avail_out, true); +		} else if (p_cmd === 'stop') { +			this.running = false; +			this.output = null; +			this.input = null; +		} else if (p_cmd === 'start_nothreads') { +			this.output = new RingBuffer(p_data[0], p_data[0].length, false); +		} else if (p_cmd === 'chunk') { +			this.output.write(p_data); +		} +	} + +	static array_has_data(arr) { +		return arr.length && arr[0].length && arr[0][0].length; +	} + +	process(inputs, outputs, parameters) { +		if (!this.running) { +			return false; // Stop processing. +		} +		if (this.output === null) { +			return true; // Not ready yet, keep processing. +		} +		const process_input = GodotProcessor.array_has_data(inputs); +		if (process_input) { +			const input = inputs[0]; +			const chunk = input[0].length * input.length; +			if (this.input_buffer.length !== chunk) { +				this.input_buffer = new Float32Array(chunk); +			} +			if (!this.threads) { +				GodotProcessor.write_input(this.input_buffer, input); +				this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer }); +			} else if (this.input.space_left() >= chunk) { +				GodotProcessor.write_input(this.input_buffer, input); +				this.input.write(this.input_buffer); +			} else { +				this.port.postMessage('Input buffer is full! Skipping input frame.'); +			} +		} +		const process_output = GodotProcessor.array_has_data(outputs); +		if (process_output) { +			const output = outputs[0]; +			const chunk = output[0].length * output.length; +			if (this.output_buffer.length !== chunk) { +				this.output_buffer = new Float32Array(chunk); +			} +			if (this.output.data_left() >= chunk) { +				this.output.read(this.output_buffer); +				GodotProcessor.write_output(output, this.output_buffer); +				if (!this.threads) { +					this.port.postMessage({ 'cmd': 'read', 'data': chunk }); +				} +			} else { +				this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); +			} +		} +		this.process_notify(); +		return true; +	} + +	static write_output(dest, source) { +		const channels = dest.length; +		for (let ch = 0; ch < channels; ch++) { +			for (let sample = 0; sample < dest[ch].length; sample++) { +				dest[ch][sample] = source[sample * channels + ch]; +			} +		} +	} + +	static write_input(dest, source) { +		const channels = source.length; +		for (let ch = 0; ch < channels; ch++) { +			for (let sample = 0; sample < source[ch].length; sample++) { +				dest[sample * channels + ch] = source[ch][sample]; +			} +		} +	} +} + +registerProcessor('godot-processor', GodotProcessor); diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js new file mode 100644 index 0000000000..756c1ac595 --- /dev/null +++ b/platform/web/js/libs/library_godot_audio.js @@ -0,0 +1,484 @@ +/*************************************************************************/ +/*  library_godot_audio.js                                               */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2022 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 GodotAudio = { +	$GodotAudio__deps: ['$GodotRuntime', '$GodotOS'], +	$GodotAudio: { +		ctx: null, +		input: null, +		driver: null, +		interval: 0, + +		init: function (mix_rate, latency, onstatechange, onlatencyupdate) { +			const opts = {}; +			// If mix_rate is 0, let the browser choose. +			if (mix_rate) { +				opts['sampleRate'] = mix_rate; +			} +			// Do not specify, leave 'interactive' for good performance. +			// opts['latencyHint'] = latency / 1000; +			const ctx = new (window.AudioContext || window.webkitAudioContext)(opts); +			GodotAudio.ctx = ctx; +			ctx.onstatechange = function () { +				let state = 0; +				switch (ctx.state) { +				case 'suspended': +					state = 0; +					break; +				case 'running': +					state = 1; +					break; +				case 'closed': +					state = 2; +					break; + +					// no default +				} +				onstatechange(state); +			}; +			ctx.onstatechange(); // Immediately notify state. +			// Update computed latency +			GodotAudio.interval = setInterval(function () { +				let computed_latency = 0; +				if (ctx.baseLatency) { +					computed_latency += GodotAudio.ctx.baseLatency; +				} +				if (ctx.outputLatency) { +					computed_latency += GodotAudio.ctx.outputLatency; +				} +				onlatencyupdate(computed_latency); +			}, 1000); +			GodotOS.atexit(GodotAudio.close_async); +			return ctx.destination.channelCount; +		}, + +		create_input: function (callback) { +			if (GodotAudio.input) { +				return 0; // Already started. +			} +			function gotMediaInput(stream) { +				try { +					GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); +					callback(GodotAudio.input); +				} catch (e) { +					GodotRuntime.error('Failed creaating input.', e); +				} +			} +			if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { +				navigator.mediaDevices.getUserMedia({ +					'audio': true, +				}).then(gotMediaInput, function (e) { +					GodotRuntime.error('Error getting user media.', e); +				}); +			} else { +				if (!navigator.getUserMedia) { +					navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; +				} +				if (!navigator.getUserMedia) { +					GodotRuntime.error('getUserMedia not available.'); +					return 1; +				} +				navigator.getUserMedia({ +					'audio': true, +				}, gotMediaInput, function (e) { +					GodotRuntime.print(e); +				}); +			} +			return 0; +		}, + +		close_async: function (resolve, reject) { +			const ctx = GodotAudio.ctx; +			GodotAudio.ctx = null; +			// Audio was not initialized. +			if (!ctx) { +				resolve(); +				return; +			} +			// Remove latency callback +			if (GodotAudio.interval) { +				clearInterval(GodotAudio.interval); +				GodotAudio.interval = 0; +			} +			// Disconnect input, if it was started. +			if (GodotAudio.input) { +				GodotAudio.input.disconnect(); +				GodotAudio.input = null; +			} +			// Disconnect output +			let closed = Promise.resolve(); +			if (GodotAudio.driver) { +				closed = GodotAudio.driver.close(); +			} +			closed.then(function () { +				return ctx.close(); +			}).then(function () { +				ctx.onstatechange = null; +				resolve(); +			}).catch(function (e) { +				ctx.onstatechange = null; +				GodotRuntime.error('Error closing AudioContext', e); +				resolve(); +			}); +		}, +	}, + +	godot_audio_is_available__sig: 'i', +	godot_audio_is_available__proxy: 'sync', +	godot_audio_is_available: function () { +		if (!(window.AudioContext || window.webkitAudioContext)) { +			return 0; +		} +		return 1; +	}, + +	godot_audio_has_worklet__sig: 'i', +	godot_audio_has_worklet: function () { +		return (GodotAudio.ctx && GodotAudio.ctx.audioWorklet) ? 1 : 0; +	}, + +	godot_audio_has_script_processor__sig: 'i', +	godot_audio_has_script_processor: function () { +		return (GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor) ? 1 : 0; +	}, + +	godot_audio_init__sig: 'iiiii', +	godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) { +		const statechange = GodotRuntime.get_func(p_state_change); +		const latencyupdate = GodotRuntime.get_func(p_latency_update); +		const mix_rate = GodotRuntime.getHeapValue(p_mix_rate, 'i32'); +		const channels = GodotAudio.init(mix_rate, p_latency, statechange, latencyupdate); +		GodotRuntime.setHeapValue(p_mix_rate, GodotAudio.ctx.sampleRate, 'i32'); +		return channels; +	}, + +	godot_audio_resume__sig: 'v', +	godot_audio_resume: function () { +		if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') { +			GodotAudio.ctx.resume(); +		} +	}, + +	godot_audio_capture_start__proxy: 'sync', +	godot_audio_capture_start__sig: 'i', +	godot_audio_capture_start: function () { +		return GodotAudio.create_input(function (input) { +			input.connect(GodotAudio.driver.get_node()); +		}); +	}, + +	godot_audio_capture_stop__proxy: 'sync', +	godot_audio_capture_stop__sig: 'v', +	godot_audio_capture_stop: function () { +		if (GodotAudio.input) { +			const tracks = GodotAudio.input['mediaStream']['getTracks'](); +			for (let i = 0; i < tracks.length; i++) { +				tracks[i]['stop'](); +			} +			GodotAudio.input.disconnect(); +			GodotAudio.input = null; +		} +	}, +}; + +autoAddDeps(GodotAudio, '$GodotAudio'); +mergeInto(LibraryManager.library, GodotAudio); + +/** + * The AudioWorklet API driver, used when threads are available. + */ +const GodotAudioWorklet = { +	$GodotAudioWorklet__deps: ['$GodotAudio', '$GodotConfig'], +	$GodotAudioWorklet: { +		promise: null, +		worklet: null, +		ring_buffer: null, + +		create: function (channels) { +			const path = GodotConfig.locate_file('godot.audio.worklet.js'); +			GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet.addModule(path).then(function () { +				GodotAudioWorklet.worklet = new AudioWorkletNode( +					GodotAudio.ctx, +					'godot-processor', +					{ +						'outputChannelCount': [channels], +					} +				); +				return Promise.resolve(); +			}); +			GodotAudio.driver = GodotAudioWorklet; +		}, + +		start: function (in_buf, out_buf, state) { +			GodotAudioWorklet.promise.then(function () { +				const node = GodotAudioWorklet.worklet; +				node.connect(GodotAudio.ctx.destination); +				node.port.postMessage({ +					'cmd': 'start', +					'data': [state, in_buf, out_buf], +				}); +				node.port.onmessage = function (event) { +					GodotRuntime.error(event.data); +				}; +			}); +		}, + +		start_no_threads: function (p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback) { +			function RingBuffer() { +				let wpos = 0; +				let rpos = 0; +				let pending_samples = 0; +				const wbuf = new Float32Array(p_out_size); + +				function send(port) { +					if (pending_samples === 0) { +						return; +					} +					const buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); +					const size = buffer.length; +					const tot_sent = pending_samples; +					out_callback(wpos, pending_samples); +					if (wpos + pending_samples >= size) { +						const high = size - wpos; +						wbuf.set(buffer.subarray(wpos, size)); +						pending_samples -= high; +						wpos = 0; +					} +					if (pending_samples > 0) { +						wbuf.set(buffer.subarray(wpos, wpos + pending_samples), tot_sent - pending_samples); +					} +					port.postMessage({ 'cmd': 'chunk', 'data': wbuf.subarray(0, tot_sent) }); +					wpos += pending_samples; +					pending_samples = 0; +				} +				this.receive = function (recv_buf) { +					const buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); +					const from = rpos; +					let to_write = recv_buf.length; +					let high = 0; +					if (rpos + to_write >= p_in_size) { +						high = p_in_size - rpos; +						buffer.set(recv_buf.subarray(0, high), rpos); +						to_write -= high; +						rpos = 0; +					} +					if (to_write) { +						buffer.set(recv_buf.subarray(high, to_write), rpos); +					} +					in_callback(from, recv_buf.length); +					rpos += to_write; +				}; +				this.consumed = function (size, port) { +					pending_samples += size; +					send(port); +				}; +			} +			GodotAudioWorklet.ring_buffer = new RingBuffer(); +			GodotAudioWorklet.promise.then(function () { +				const node = GodotAudioWorklet.worklet; +				const buffer = GodotRuntime.heapSlice(HEAPF32, p_out_buf, p_out_size); +				node.connect(GodotAudio.ctx.destination); +				node.port.postMessage({ +					'cmd': 'start_nothreads', +					'data': [buffer, p_in_size], +				}); +				node.port.onmessage = function (event) { +					if (!GodotAudioWorklet.worklet) { +						return; +					} +					if (event.data['cmd'] === 'read') { +						const read = event.data['data']; +						GodotAudioWorklet.ring_buffer.consumed(read, GodotAudioWorklet.worklet.port); +					} else if (event.data['cmd'] === 'input') { +						const buf = event.data['data']; +						if (buf.length > p_in_size) { +							GodotRuntime.error('Input chunk is too big'); +							return; +						} +						GodotAudioWorklet.ring_buffer.receive(buf); +					} else { +						GodotRuntime.error(event.data); +					} +				}; +			}); +		}, + +		get_node: function () { +			return GodotAudioWorklet.worklet; +		}, + +		close: function () { +			return new Promise(function (resolve, reject) { +				if (GodotAudioWorklet.promise === null) { +					return; +				} +				GodotAudioWorklet.promise.then(function () { +					GodotAudioWorklet.worklet.port.postMessage({ +						'cmd': 'stop', +						'data': null, +					}); +					GodotAudioWorklet.worklet.disconnect(); +					GodotAudioWorklet.worklet = null; +					GodotAudioWorklet.promise = null; +					resolve(); +				}).catch(function (err) { /* aborted? */ }); +			}); +		}, +	}, + +	godot_audio_worklet_create__sig: 'ii', +	godot_audio_worklet_create: function (channels) { +		try { +			GodotAudioWorklet.create(channels); +		} catch (e) { +			GodotRuntime.error('Error starting AudioDriverWorklet', e); +			return 1; +		} +		return 0; +	}, + +	godot_audio_worklet_start__sig: 'viiiii', +	godot_audio_worklet_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) { +		const out_buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); +		const in_buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); +		const state = GodotRuntime.heapSub(HEAP32, p_state, 4); +		GodotAudioWorklet.start(in_buffer, out_buffer, state); +	}, + +	godot_audio_worklet_start_no_threads__sig: 'viiiiii', +	godot_audio_worklet_start_no_threads: function (p_out_buf, p_out_size, p_out_callback, p_in_buf, p_in_size, p_in_callback) { +		const out_callback = GodotRuntime.get_func(p_out_callback); +		const in_callback = GodotRuntime.get_func(p_in_callback); +		GodotAudioWorklet.start_no_threads(p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback); +	}, + +	godot_audio_worklet_state_wait__sig: 'iiii', +	godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) { +		Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout); +		return Atomics.load(HEAP32, (p_state >> 2) + p_idx); +	}, + +	godot_audio_worklet_state_add__sig: 'iiii', +	godot_audio_worklet_state_add: function (p_state, p_idx, p_value) { +		return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value); +	}, + +	godot_audio_worklet_state_get__sig: 'iii', +	godot_audio_worklet_state_get: function (p_state, p_idx) { +		return Atomics.load(HEAP32, (p_state >> 2) + p_idx); +	}, +}; + +autoAddDeps(GodotAudioWorklet, '$GodotAudioWorklet'); +mergeInto(LibraryManager.library, GodotAudioWorklet); + +/* + * The deprecated ScriptProcessorNode API, used when threads are disabled. + */ +const GodotAudioScript = { +	$GodotAudioScript__deps: ['$GodotAudio'], +	$GodotAudioScript: { +		script: null, + +		create: function (buffer_length, channel_count) { +			GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); +			GodotAudio.driver = GodotAudioScript; +			return GodotAudioScript.script.bufferSize; +		}, + +		start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess) { +			GodotAudioScript.script.onaudioprocess = function (event) { +				// Read input +				const inb = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); +				const input = event.inputBuffer; +				if (GodotAudio.input) { +					const inlen = input.getChannelData(0).length; +					for (let ch = 0; ch < 2; ch++) { +						const data = input.getChannelData(ch); +						for (let s = 0; s < inlen; s++) { +							inb[s * 2 + ch] = data[s]; +						} +					} +				} + +				// Let Godot process the input/output. +				onprocess(); + +				// Write the output. +				const outb = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); +				const output = event.outputBuffer; +				const channels = output.numberOfChannels; +				for (let ch = 0; ch < channels; ch++) { +					const data = output.getChannelData(ch); +					// Loop through samples and assign computed values. +					for (let sample = 0; sample < data.length; sample++) { +						data[sample] = outb[sample * channels + ch]; +					} +				} +			}; +			GodotAudioScript.script.connect(GodotAudio.ctx.destination); +		}, + +		get_node: function () { +			return GodotAudioScript.script; +		}, + +		close: function () { +			return new Promise(function (resolve, reject) { +				GodotAudioScript.script.disconnect(); +				GodotAudioScript.script.onaudioprocess = null; +				GodotAudioScript.script = null; +				resolve(); +			}); +		}, +	}, + +	godot_audio_script_create__sig: 'iii', +	godot_audio_script_create: function (buffer_length, channel_count) { +		const buf_len = GodotRuntime.getHeapValue(buffer_length, 'i32'); +		try { +			const out_len = GodotAudioScript.create(buf_len, channel_count); +			GodotRuntime.setHeapValue(buffer_length, out_len, 'i32'); +		} catch (e) { +			GodotRuntime.error('Error starting AudioDriverScriptProcessor', e); +			return 1; +		} +		return 0; +	}, + +	godot_audio_script_start__sig: 'viiiii', +	godot_audio_script_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) { +		const onprocess = GodotRuntime.get_func(p_cb); +		GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess); +	}, +}; + +autoAddDeps(GodotAudioScript, '$GodotAudioScript'); +mergeInto(LibraryManager.library, GodotAudioScript); diff --git a/platform/web/js/libs/library_godot_display.js b/platform/web/js/libs/library_godot_display.js new file mode 100644 index 0000000000..91cb8e728a --- /dev/null +++ b/platform/web/js/libs/library_godot_display.js @@ -0,0 +1,754 @@ +/*************************************************************************/ +/*  library_godot_display.js                                             */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2022 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 GodotDisplayVK = { + +	$GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners'], +	$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; +				GodotEventListeners.add(elem, 'input', function (evt) { +					const c_str = GodotRuntime.allocString(elem.value); +					input_cb(c_str, elem.selectionEnd); +					GodotRuntime.free(c_str); +				}, false); +				GodotEventListeners.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, type, start, end) { +			if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) { +				return; +			} +			if (GodotDisplayVK.textinput.style.display !== '' || GodotDisplayVK.textarea.style.display !== '') { +				GodotDisplayVK.hide(); +			} +			GodotDisplayVK.updateSize(); + +			let elem = GodotDisplayVK.textinput; +			switch (type) { +			case 0: // KEYBOARD_TYPE_DEFAULT +				elem.type = 'text'; +				elem.inputmode = ''; +				break; +			case 1: // KEYBOARD_TYPE_MULTILINE +				elem = GodotDisplayVK.textarea; +				break; +			case 2: // KEYBOARD_TYPE_NUMBER +				elem.type = 'text'; +				elem.inputmode = 'numeric'; +				break; +			case 3: // KEYBOARD_TYPE_NUMBER_DECIMAL +				elem.type = 'text'; +				elem.inputmode = 'decimal'; +				break; +			case 4: // KEYBOARD_TYPE_PHONE +				elem.type = 'tel'; +				elem.inputmode = ''; +				break; +			case 5: // KEYBOARD_TYPE_EMAIL_ADDRESS +				elem.type = 'email'; +				elem.inputmode = ''; +				break; +			case 6: // KEYBOARD_TYPE_PASSWORD +				elem.type = 'password'; +				elem.inputmode = ''; +				break; +			case 7: // KEYBOARD_TYPE_URL +				elem.type = 'url'; +				elem.inputmode = ''; +				break; +			default: +				elem.type = 'text'; +				elem.inputmode = ''; +				break; +			} + +			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. + */ +const GodotDisplayCursor = { +	$GodotDisplayCursor__deps: ['$GodotOS', '$GodotConfig'], +	$GodotDisplayCursor__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayCursor.clear(); resolve(); });', +	$GodotDisplayCursor: { +		shape: 'auto', +		visible: true, +		cursors: {}, +		set_style: function (style) { +			GodotConfig.canvas.style.cursor = style; +		}, +		set_shape: function (shape) { +			GodotDisplayCursor.shape = shape; +			let css = shape; +			if (shape in GodotDisplayCursor.cursors) { +				const c = GodotDisplayCursor.cursors[shape]; +				css = `url("${c.url}") ${c.x} ${c.y}, auto`; +			} +			if (GodotDisplayCursor.visible) { +				GodotDisplayCursor.set_style(css); +			} +		}, +		clear: function () { +			GodotDisplayCursor.set_style(''); +			GodotDisplayCursor.shape = 'auto'; +			GodotDisplayCursor.visible = true; +			Object.keys(GodotDisplayCursor.cursors).forEach(function (key) { +				URL.revokeObjectURL(GodotDisplayCursor.cursors[key]); +				delete GodotDisplayCursor.cursors[key]; +			}); +		}, +		lockPointer: function () { +			const canvas = GodotConfig.canvas; +			if (canvas.requestPointerLock) { +				canvas.requestPointerLock(); +			} +		}, +		releasePointer: function () { +			if (document.exitPointerLock) { +				document.exitPointerLock(); +			} +		}, +		isPointerLocked: function () { +			return document.pointerLockElement === GodotConfig.canvas; +		}, +	}, +}; +mergeInto(LibraryManager.library, GodotDisplayCursor); + +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', '$GodotEventListeners', '$GodotDisplayScreen', '$GodotDisplayVK'], +	$GodotDisplay: { +		window_icon: '', +		getDPI: function () { +			// devicePixelRatio is given in dppx +			// https://drafts.csswg.org/css-values/#resolution +			// > due to the 1:96 fixed ratio of CSS *in* to CSS *px*, 1dppx is equivalent to 96dpi. +			const dpi = Math.round(window.devicePixelRatio * 96); +			return dpi >= 96 ? dpi : 96; +		}, +	}, + +	godot_js_display_is_swap_ok_cancel__sig: 'i', +	godot_js_display_is_swap_ok_cancel: function () { +		const win = (['Windows', 'Win64', 'Win32', 'WinCE']); +		const plat = navigator.platform || ''; +		if (win.indexOf(plat) !== -1) { +			return 1; +		} +		return 0; +	}, + +	godot_js_tts_is_speaking__sig: 'i', +	godot_js_tts_is_speaking: function () { +		return window.speechSynthesis.speaking; +	}, + +	godot_js_tts_is_paused__sig: 'i', +	godot_js_tts_is_paused: function () { +		return window.speechSynthesis.paused; +	}, + +	godot_js_tts_get_voices__sig: 'vi', +	godot_js_tts_get_voices: function (p_callback) { +		const func = GodotRuntime.get_func(p_callback); +		try { +			const arr = []; +			const voices = window.speechSynthesis.getVoices(); +			for (let i = 0; i < voices.length; i++) { +				arr.push(`${voices[i].lang};${voices[i].name}`); +			} +			const c_ptr = GodotRuntime.allocStringArray(arr); +			func(arr.length, c_ptr); +			GodotRuntime.freeStringArray(c_ptr, arr.length); +		} catch (e) { +			// Fail graciously. +		} +	}, + +	godot_js_tts_speak__sig: 'viiiffii', +	godot_js_tts_speak: function (p_text, p_voice, p_volume, p_pitch, p_rate, p_utterance_id, p_callback) { +		const func = GodotRuntime.get_func(p_callback); + +		function listener_end(evt) { +			evt.currentTarget.cb(1 /*TTS_UTTERANCE_ENDED*/, evt.currentTarget.id, 0); +		} + +		function listener_start(evt) { +			evt.currentTarget.cb(0 /*TTS_UTTERANCE_STARTED*/, evt.currentTarget.id, 0); +		} + +		function listener_error(evt) { +			evt.currentTarget.cb(2 /*TTS_UTTERANCE_CANCELED*/, evt.currentTarget.id, 0); +		} + +		function listener_bound(evt) { +			evt.currentTarget.cb(3 /*TTS_UTTERANCE_BOUNDARY*/, evt.currentTarget.id, evt.charIndex); +		} + +		const utterance = new SpeechSynthesisUtterance(GodotRuntime.parseString(p_text)); +		utterance.rate = p_rate; +		utterance.pitch = p_pitch; +		utterance.volume = p_volume / 100.0; +		utterance.addEventListener('end', listener_end); +		utterance.addEventListener('start', listener_start); +		utterance.addEventListener('error', listener_error); +		utterance.addEventListener('boundary', listener_bound); +		utterance.id = p_utterance_id; +		utterance.cb = func; +		const voice = GodotRuntime.parseString(p_voice); +		const voices = window.speechSynthesis.getVoices(); +		for (let i = 0; i < voices.length; i++) { +			if (voices[i].name === voice) { +				utterance.voice = voices[i]; +				break; +			} +		} +		window.speechSynthesis.resume(); +		window.speechSynthesis.speak(utterance); +	}, + +	godot_js_tts_pause__sig: 'v', +	godot_js_tts_pause: function () { +		window.speechSynthesis.pause(); +	}, + +	godot_js_tts_resume__sig: 'v', +	godot_js_tts_resume: function () { +		window.speechSynthesis.resume(); +	}, + +	godot_js_tts_stop__sig: 'v', +	godot_js_tts_stop: function () { +		window.speechSynthesis.cancel(); +		window.speechSynthesis.resume(); +	}, + +	godot_js_display_alert__sig: 'vi', +	godot_js_display_alert: function (p_text) { +		window.alert(GodotRuntime.parseString(p_text)); // eslint-disable-line no-alert +	}, + +	godot_js_display_screen_dpi_get__sig: 'i', +	godot_js_display_screen_dpi_get: function () { +		return GodotDisplay.getDPI(); +	}, + +	godot_js_display_pixel_ratio_get__sig: 'f', +	godot_js_display_pixel_ratio_get: function () { +		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: 'vii', +	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__sig: 'vii', +	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_has_webgl__sig: 'ii', +	godot_js_display_has_webgl: function (p_version) { +		if (p_version !== 1 && p_version !== 2) { +			return false; +		} +		try { +			return !!document.createElement('canvas').getContext(p_version === 2 ? 'webgl2' : 'webgl'); +		} catch (e) { /* Not available */ } +		return false; +	}, + +	/* +	 * Canvas +	 */ +	godot_js_display_canvas_focus__sig: 'v', +	godot_js_display_canvas_focus: function () { +		GodotConfig.canvas.focus(); +	}, + +	godot_js_display_canvas_is_focused__sig: 'i', +	godot_js_display_canvas_is_focused: function () { +		return document.activeElement === GodotConfig.canvas; +	}, + +	/* +	 * Touchscreen +	 */ +	godot_js_display_touchscreen_is_available__sig: 'i', +	godot_js_display_touchscreen_is_available: function () { +		return 'ontouchstart' in window; +	}, + +	/* +	 * Clipboard +	 */ +	godot_js_display_clipboard_set__sig: 'ii', +	godot_js_display_clipboard_set: function (p_text) { +		const text = GodotRuntime.parseString(p_text); +		if (!navigator.clipboard || !navigator.clipboard.writeText) { +			return 1; +		} +		navigator.clipboard.writeText(text).catch(function (e) { +			// Setting OS clipboard is only possible from an input callback. +			GodotRuntime.error('Setting OS clipboard is only possible from an input callback for the Web plafrom. Exception:', e); +		}); +		return 0; +	}, + +	godot_js_display_clipboard_get__sig: 'ii', +	godot_js_display_clipboard_get: function (callback) { +		const func = GodotRuntime.get_func(callback); +		try { +			navigator.clipboard.readText().then(function (result) { +				const ptr = GodotRuntime.allocString(result); +				func(ptr); +				GodotRuntime.free(ptr); +			}).catch(function (e) { +				// Fail graciously. +			}); +		} catch (e) { +			// Fail graciously. +		} +	}, + +	/* +	 * Window +	 */ +	godot_js_display_window_title_set__sig: 'vi', +	godot_js_display_window_title_set: function (p_data) { +		document.title = GodotRuntime.parseString(p_data); +	}, + +	godot_js_display_window_icon_set__sig: 'vii', +	godot_js_display_window_icon_set: function (p_ptr, p_len) { +		let link = document.getElementById('-gd-engine-icon'); +		if (link === null) { +			link = document.createElement('link'); +			link.rel = 'icon'; +			link.id = '-gd-engine-icon'; +			document.head.appendChild(link); +		} +		const old_icon = GodotDisplay.window_icon; +		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) { +			URL.revokeObjectURL(old_icon); +		} +	}, + +	/* +	 * Cursor +	 */ +	godot_js_display_cursor_set_visible__sig: 'vi', +	godot_js_display_cursor_set_visible: function (p_visible) { +		const visible = p_visible !== 0; +		if (visible === GodotDisplayCursor.visible) { +			return; +		} +		GodotDisplayCursor.visible = visible; +		if (visible) { +			GodotDisplayCursor.set_shape(GodotDisplayCursor.shape); +		} else { +			GodotDisplayCursor.set_style('none'); +		} +	}, + +	godot_js_display_cursor_is_hidden__sig: 'i', +	godot_js_display_cursor_is_hidden: function () { +		return !GodotDisplayCursor.visible; +	}, + +	godot_js_display_cursor_set_shape__sig: 'vi', +	godot_js_display_cursor_set_shape: function (p_string) { +		GodotDisplayCursor.set_shape(GodotRuntime.parseString(p_string)); +	}, + +	godot_js_display_cursor_set_custom_shape__sig: 'viiiii', +	godot_js_display_cursor_set_custom_shape: function (p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y) { +		const shape = GodotRuntime.parseString(p_shape); +		const old_shape = GodotDisplayCursor.cursors[shape]; +		if (p_len > 0) { +			const png = new Blob([GodotRuntime.heapSlice(HEAPU8, p_ptr, p_len)], { type: 'image/png' }); +			const url = URL.createObjectURL(png); +			GodotDisplayCursor.cursors[shape] = { +				url: url, +				x: p_hotspot_x, +				y: p_hotspot_y, +			}; +		} else { +			delete GodotDisplayCursor.cursors[shape]; +		} +		if (shape === GodotDisplayCursor.shape) { +			GodotDisplayCursor.set_shape(GodotDisplayCursor.shape); +		} +		if (old_shape) { +			URL.revokeObjectURL(old_shape.url); +		} +	}, + +	godot_js_display_cursor_lock_set__sig: 'vi', +	godot_js_display_cursor_lock_set: function (p_lock) { +		if (p_lock) { +			GodotDisplayCursor.lockPointer(); +		} else { +			GodotDisplayCursor.releasePointer(); +		} +	}, + +	godot_js_display_cursor_is_locked__sig: 'i', +	godot_js_display_cursor_is_locked: function () { +		return GodotDisplayCursor.isPointerLocked() ? 1 : 0; +	}, + +	/* +	 * Listeners +	 */ +	godot_js_display_fullscreen_cb__sig: 'vi', +	godot_js_display_fullscreen_cb: function (callback) { +		const canvas = GodotConfig.canvas; +		const func = GodotRuntime.get_func(callback); +		function change_cb(evt) { +			if (evt.target === canvas) { +				func(GodotDisplayScreen.isFullscreen()); +			} +		} +		GodotEventListeners.add(document, 'fullscreenchange', change_cb, false); +		GodotEventListeners.add(document, 'mozfullscreenchange', change_cb, false); +		GodotEventListeners.add(document, 'webkitfullscreenchange', change_cb, false); +	}, + +	godot_js_display_window_blur_cb__sig: 'vi', +	godot_js_display_window_blur_cb: function (callback) { +		const func = GodotRuntime.get_func(callback); +		GodotEventListeners.add(window, 'blur', function () { +			func(); +		}, false); +	}, + +	godot_js_display_notification_cb__sig: 'viiiii', +	godot_js_display_notification_cb: function (callback, p_enter, p_exit, p_in, p_out) { +		const canvas = GodotConfig.canvas; +		const func = GodotRuntime.get_func(callback); +		const notif = [p_enter, p_exit, p_in, p_out]; +		['mouseover', 'mouseleave', 'focus', 'blur'].forEach(function (evt_name, idx) { +			GodotEventListeners.add(canvas, evt_name, function () { +				func(notif[idx]); +			}, true); +		}); +	}, + +	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; +		GodotEventListeners.add(canvas, 'contextmenu', function (ev) { +			ev.preventDefault(); +		}, false); +		GodotEventListeners.add(canvas, 'webglcontextlost', function (ev) { +			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_type, 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_type, 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_tts_available__sig: 'i', +	godot_js_display_tts_available: function () { +		return 'speechSynthesis' in window; +	}, + +	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); +		} +	}, +}; + +autoAddDeps(GodotDisplay, '$GodotDisplay'); +mergeInto(LibraryManager.library, GodotDisplay); diff --git a/platform/web/js/libs/library_godot_fetch.js b/platform/web/js/libs/library_godot_fetch.js new file mode 100644 index 0000000000..285e50a035 --- /dev/null +++ b/platform/web/js/libs/library_godot_fetch.js @@ -0,0 +1,247 @@ +/*************************************************************************/ +/*  library_godot_fetch.js                                               */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2022 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: ['$IDHandler', '$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 chunked = false; +			response.headers.forEach(function (value, header) { +				const v = value.toLowerCase().trim(); +				const h = header.toLowerCase().trim(); +				if (h === 'transfer-encoding' && v === 'chunked') { +					chunked = true; +				} +			}); +			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: 'iiiiiii', +	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: 'iiii', +	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: 'iiii', +	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/web/js/libs/library_godot_input.js b/platform/web/js/libs/library_godot_input.js new file mode 100644 index 0000000000..51571d64a2 --- /dev/null +++ b/platform/web/js/libs/library_godot_input.js @@ -0,0 +1,549 @@ +/*************************************************************************/ +/*  library_godot_input.js                                               */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2022 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.                */ +/*************************************************************************/ + +/* + * Gamepad API helper. + */ +const GodotInputGamepads = { +	$GodotInputGamepads__deps: ['$GodotRuntime', '$GodotEventListeners'], +	$GodotInputGamepads: { +		samples: [], + +		get_pads: function () { +			try { +				// Will throw in iframe when permission is denied. +				// Will throw/warn in the future for insecure contexts. +				// See https://github.com/w3c/gamepad/pull/120 +				const pads = navigator.getGamepads(); +				if (pads) { +					return pads; +				} +				return []; +			} catch (e) { +				return []; +			} +		}, + +		get_samples: function () { +			return GodotInputGamepads.samples; +		}, + +		get_sample: function (index) { +			const samples = GodotInputGamepads.samples; +			return index < samples.length ? samples[index] : null; +		}, + +		sample: function () { +			const pads = GodotInputGamepads.get_pads(); +			const samples = []; +			for (let i = 0; i < pads.length; i++) { +				const pad = pads[i]; +				if (!pad) { +					samples.push(null); +					continue; +				} +				const s = { +					standard: pad.mapping === 'standard', +					buttons: [], +					axes: [], +					connected: pad.connected, +				}; +				for (let b = 0; b < pad.buttons.length; b++) { +					s.buttons.push(pad.buttons[b].value); +				} +				for (let a = 0; a < pad.axes.length; a++) { +					s.axes.push(pad.axes[a]); +				} +				samples.push(s); +			} +			GodotInputGamepads.samples = samples; +		}, + +		init: function (onchange) { +			GodotInputGamepads.samples = []; +			function add(pad) { +				const guid = GodotInputGamepads.get_guid(pad); +				const c_id = GodotRuntime.allocString(pad.id); +				const c_guid = GodotRuntime.allocString(guid); +				onchange(pad.index, 1, c_id, c_guid); +				GodotRuntime.free(c_id); +				GodotRuntime.free(c_guid); +			} +			const pads = GodotInputGamepads.get_pads(); +			for (let i = 0; i < pads.length; i++) { +				// Might be reserved space. +				if (pads[i]) { +					add(pads[i]); +				} +			} +			GodotEventListeners.add(window, 'gamepadconnected', function (evt) { +				if (evt.gamepad) { +					add(evt.gamepad); +				} +			}, false); +			GodotEventListeners.add(window, 'gamepaddisconnected', function (evt) { +				if (evt.gamepad) { +					onchange(evt.gamepad.index, 0); +				} +			}, false); +		}, + +		get_guid: function (pad) { +			if (pad.mapping) { +				return pad.mapping; +			} +			const ua = navigator.userAgent; +			let os = 'Unknown'; +			if (ua.indexOf('Android') >= 0) { +				os = 'Android'; +			} else if (ua.indexOf('Linux') >= 0) { +				os = 'Linux'; +			} else if (ua.indexOf('iPhone') >= 0) { +				os = 'iOS'; +			} else if (ua.indexOf('Macintosh') >= 0) { +				// Updated iPads will fall into this category. +				os = 'MacOSX'; +			} else if (ua.indexOf('Windows') >= 0) { +				os = 'Windows'; +			} + +			const id = pad.id; +			// Chrom* style: NAME (Vendor: xxxx Product: xxxx) +			const exp1 = /vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i; +			// Firefox/Safari style (safari may remove leading zeores) +			const exp2 = /^([0-9a-f]+)-([0-9a-f]+)-/i; +			let vendor = ''; +			let product = ''; +			if (exp1.test(id)) { +				const match = exp1.exec(id); +				vendor = match[1].padStart(4, '0'); +				product = match[2].padStart(4, '0'); +			} else if (exp2.test(id)) { +				const match = exp2.exec(id); +				vendor = match[1].padStart(4, '0'); +				product = match[2].padStart(4, '0'); +			} +			if (!vendor || !product) { +				return `${os}Unknown`; +			} +			return os + vendor + product; +		}, +	}, +}; +mergeInto(LibraryManager.library, GodotInputGamepads); + +/* + * Drag and drop helper. + * This is pretty big, but basically detect dropped files on GodotConfig.canvas, + * process them one by one (recursively for directories), and copies them to + * the temporary FS path '/tmp/drop-[random]/' so it can be emitted as a godot + * event (that requires a string array of paths). + * + * NOTE: The temporary files are removed after the callback. This means that + * deferred callbacks won't be able to access the files. + */ +const GodotInputDragDrop = { +	$GodotInputDragDrop__deps: ['$FS', '$GodotFS'], +	$GodotInputDragDrop: { +		promises: [], +		pending_files: [], + +		add_entry: function (entry) { +			if (entry.isDirectory) { +				GodotInputDragDrop.add_dir(entry); +			} else if (entry.isFile) { +				GodotInputDragDrop.add_file(entry); +			} else { +				GodotRuntime.error('Unrecognized entry...', entry); +			} +		}, + +		add_dir: function (entry) { +			GodotInputDragDrop.promises.push(new Promise(function (resolve, reject) { +				const reader = entry.createReader(); +				reader.readEntries(function (entries) { +					for (let i = 0; i < entries.length; i++) { +						GodotInputDragDrop.add_entry(entries[i]); +					} +					resolve(); +				}); +			})); +		}, + +		add_file: function (entry) { +			GodotInputDragDrop.promises.push(new Promise(function (resolve, reject) { +				entry.file(function (file) { +					const reader = new FileReader(); +					reader.onload = function () { +						const f = { +							'path': file.relativePath || file.webkitRelativePath, +							'name': file.name, +							'type': file.type, +							'size': file.size, +							'data': reader.result, +						}; +						if (!f['path']) { +							f['path'] = f['name']; +						} +						GodotInputDragDrop.pending_files.push(f); +						resolve(); +					}; +					reader.onerror = function () { +						GodotRuntime.print('Error reading file'); +						reject(); +					}; +					reader.readAsArrayBuffer(file); +				}, function (err) { +					GodotRuntime.print('Error!'); +					reject(); +				}); +			})); +		}, + +		process: function (resolve, reject) { +			if (GodotInputDragDrop.promises.length === 0) { +				resolve(); +				return; +			} +			GodotInputDragDrop.promises.pop().then(function () { +				setTimeout(function () { +					GodotInputDragDrop.process(resolve, reject); +				}, 0); +			}); +		}, + +		_process_event: function (ev, callback) { +			ev.preventDefault(); +			if (ev.dataTransfer.items) { +				// Use DataTransferItemList interface to access the file(s) +				for (let i = 0; i < ev.dataTransfer.items.length; i++) { +					const item = ev.dataTransfer.items[i]; +					let entry = null; +					if ('getAsEntry' in item) { +						entry = item.getAsEntry(); +					} else if ('webkitGetAsEntry' in item) { +						entry = item.webkitGetAsEntry(); +					} +					if (entry) { +						GodotInputDragDrop.add_entry(entry); +					} +				} +			} else { +				GodotRuntime.error('File upload not supported'); +			} +			new Promise(GodotInputDragDrop.process).then(function () { +				const DROP = `/tmp/drop-${parseInt(Math.random() * (1 << 30), 10)}/`; +				const drops = []; +				const files = []; +				FS.mkdir(DROP.slice(0, -1)); // Without trailing slash +				GodotInputDragDrop.pending_files.forEach((elem) => { +					const path = elem['path']; +					GodotFS.copy_to_fs(DROP + path, elem['data']); +					let idx = path.indexOf('/'); +					if (idx === -1) { +						// Root file +						drops.push(DROP + path); +					} else { +						// Subdir +						const sub = path.substr(0, idx); +						idx = sub.indexOf('/'); +						if (idx < 0 && drops.indexOf(DROP + sub) === -1) { +							drops.push(DROP + sub); +						} +					} +					files.push(DROP + path); +				}); +				GodotInputDragDrop.promises = []; +				GodotInputDragDrop.pending_files = []; +				callback(drops); +				if (GodotConfig.persistent_drops) { +					// Delay removal at exit. +					GodotOS.atexit(function (resolve, reject) { +						GodotInputDragDrop.remove_drop(files, DROP); +						resolve(); +					}); +				} else { +					GodotInputDragDrop.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); +					} +					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); +			}); +		}, + +		handler: function (callback) { +			return function (ev) { +				GodotInputDragDrop._process_event(ev, callback); +			}; +		}, +	}, +}; +mergeInto(LibraryManager.library, GodotInputDragDrop); + +/* + * Godot exposed input functions. + */ +const GodotInput = { +	$GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop'], +	$GodotInput: { +		getModifiers: function (evt) { +			return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3); +		}, +		computePosition: function (evt, rect) { +			const canvas = GodotConfig.canvas; +			const rw = canvas.width / rect.width; +			const rh = canvas.height / rect.height; +			const x = (evt.clientX - rect.x) * rw; +			const y = (evt.clientY - rect.y) * rh; +			return [x, y]; +		}, +	}, + +	/* +	 * Mouse API +	 */ +	godot_js_input_mouse_move_cb__sig: 'vi', +	godot_js_input_mouse_move_cb: function (callback) { +		const func = GodotRuntime.get_func(callback); +		const canvas = GodotConfig.canvas; +		function move_cb(evt) { +			const rect = canvas.getBoundingClientRect(); +			const pos = GodotInput.computePosition(evt, rect); +			// Scale movement +			const rw = canvas.width / rect.width; +			const rh = canvas.height / rect.height; +			const rel_pos_x = evt.movementX * rw; +			const rel_pos_y = evt.movementY * rh; +			const modifiers = GodotInput.getModifiers(evt); +			func(pos[0], pos[1], rel_pos_x, rel_pos_y, modifiers); +		} +		GodotEventListeners.add(window, 'mousemove', move_cb, false); +	}, + +	godot_js_input_mouse_wheel_cb__sig: 'vi', +	godot_js_input_mouse_wheel_cb: function (callback) { +		const func = GodotRuntime.get_func(callback); +		function wheel_cb(evt) { +			if (func(evt['deltaX'] || 0, evt['deltaY'] || 0)) { +				evt.preventDefault(); +			} +		} +		GodotEventListeners.add(GodotConfig.canvas, 'wheel', wheel_cb, false); +	}, + +	godot_js_input_mouse_button_cb__sig: 'vi', +	godot_js_input_mouse_button_cb: function (callback) { +		const func = GodotRuntime.get_func(callback); +		const canvas = GodotConfig.canvas; +		function button_cb(p_pressed, evt) { +			const rect = canvas.getBoundingClientRect(); +			const pos = GodotInput.computePosition(evt, rect); +			const modifiers = GodotInput.getModifiers(evt); +			// Since the event is consumed, focus manually. +			// NOTE: The iframe container may not have focus yet, so focus even when already active. +			if (p_pressed) { +				GodotConfig.canvas.focus(); +			} +			if (func(p_pressed, evt.button, pos[0], pos[1], modifiers)) { +				evt.preventDefault(); +			} +		} +		GodotEventListeners.add(canvas, 'mousedown', button_cb.bind(null, 1), false); +		GodotEventListeners.add(window, 'mouseup', button_cb.bind(null, 0), false); +	}, + +	/* +	 * Touch API +	 */ +	godot_js_input_touch_cb__sig: 'viii', +	godot_js_input_touch_cb: function (callback, ids, coords) { +		const func = GodotRuntime.get_func(callback); +		const canvas = GodotConfig.canvas; +		function touch_cb(type, evt) { +			// Since the event is consumed, focus manually. +			// NOTE: The iframe container may not have focus yet, so focus even when already active. +			if (type === 0) { +				GodotConfig.canvas.focus(); +			} +			const rect = canvas.getBoundingClientRect(); +			const touches = evt.changedTouches; +			for (let i = 0; i < touches.length; i++) { +				const touch = touches[i]; +				const pos = GodotInput.computePosition(touch, rect); +				GodotRuntime.setHeapValue(coords + (i * 2) * 8, pos[0], 'double'); +				GodotRuntime.setHeapValue(coords + (i * 2 + 1) * 8, pos[1], 'double'); +				GodotRuntime.setHeapValue(ids + i * 4, touch.identifier, 'i32'); +			} +			func(type, touches.length); +			if (evt.cancelable) { +				evt.preventDefault(); +			} +		} +		GodotEventListeners.add(canvas, 'touchstart', touch_cb.bind(null, 0), false); +		GodotEventListeners.add(canvas, 'touchend', touch_cb.bind(null, 1), false); +		GodotEventListeners.add(canvas, 'touchcancel', touch_cb.bind(null, 1), false); +		GodotEventListeners.add(canvas, 'touchmove', touch_cb.bind(null, 2), false); +	}, + +	/* +	 * Key API +	 */ +	godot_js_input_key_cb__sig: 'viii', +	godot_js_input_key_cb: function (callback, code, key) { +		const func = GodotRuntime.get_func(callback); +		function key_cb(pressed, evt) { +			const modifiers = GodotInput.getModifiers(evt); +			GodotRuntime.stringToHeap(evt.code, code, 32); +			GodotRuntime.stringToHeap(evt.key, key, 32); +			func(pressed, evt.repeat, modifiers); +			evt.preventDefault(); +		} +		GodotEventListeners.add(GodotConfig.canvas, 'keydown', key_cb.bind(null, 1), false); +		GodotEventListeners.add(GodotConfig.canvas, 'keyup', key_cb.bind(null, 0), false); +	}, + +	/* +	 * Gamepad API +	 */ +	godot_js_input_gamepad_cb__sig: 'vi', +	godot_js_input_gamepad_cb: function (change_cb) { +		const onchange = GodotRuntime.get_func(change_cb); +		GodotInputGamepads.init(onchange); +	}, + +	godot_js_input_gamepad_sample_count__sig: 'i', +	godot_js_input_gamepad_sample_count: function () { +		return GodotInputGamepads.get_samples().length; +	}, + +	godot_js_input_gamepad_sample__sig: 'i', +	godot_js_input_gamepad_sample: function () { +		GodotInputGamepads.sample(); +		return 0; +	}, + +	godot_js_input_gamepad_sample_get__sig: 'iiiiiii', +	godot_js_input_gamepad_sample_get: function (p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard) { +		const sample = GodotInputGamepads.get_sample(p_index); +		if (!sample || !sample.connected) { +			return 1; +		} +		const btns = sample.buttons; +		const btns_len = btns.length < 16 ? btns.length : 16; +		for (let i = 0; i < btns_len; i++) { +			GodotRuntime.setHeapValue(r_btns + (i << 2), btns[i], 'float'); +		} +		GodotRuntime.setHeapValue(r_btns_num, btns_len, 'i32'); +		const axes = sample.axes; +		const axes_len = axes.length < 10 ? axes.length : 10; +		for (let i = 0; i < axes_len; i++) { +			GodotRuntime.setHeapValue(r_axes + (i << 2), axes[i], 'float'); +		} +		GodotRuntime.setHeapValue(r_axes_num, axes_len, 'i32'); +		const is_standard = sample.standard ? 1 : 0; +		GodotRuntime.setHeapValue(r_standard, is_standard, 'i32'); +		return 0; +	}, + +	/* +	 * Drag/Drop API +	 */ +	godot_js_input_drop_files_cb__sig: 'vi', +	godot_js_input_drop_files_cb: function (callback) { +		const func = GodotRuntime.get_func(callback); +		const dropFiles = function (files) { +			const args = files || []; +			if (!args.length) { +				return; +			} +			const argc = args.length; +			const argv = GodotRuntime.allocStringArray(args); +			func(argv, argc); +			GodotRuntime.freeStringArray(argv, argc); +		}; +		const canvas = GodotConfig.canvas; +		GodotEventListeners.add(canvas, 'dragover', function (ev) { +			// Prevent default behavior (which would try to open the file(s)) +			ev.preventDefault(); +		}, false); +		GodotEventListeners.add(canvas, 'drop', GodotInputDragDrop.handler(dropFiles)); +	}, + +	/* Paste API */ +	godot_js_input_paste_cb__sig: 'vi', +	godot_js_input_paste_cb: function (callback) { +		const func = GodotRuntime.get_func(callback); +		GodotEventListeners.add(window, 'paste', function (evt) { +			const text = evt.clipboardData.getData('text'); +			const ptr = GodotRuntime.allocString(text); +			func(ptr); +			GodotRuntime.free(ptr); +		}, false); +	}, + +	godot_js_input_vibrate_handheld__sig: 'vi', +	godot_js_input_vibrate_handheld: function (p_duration_ms) { +		if (typeof navigator.vibrate !== 'function') { +			GodotRuntime.print('This browser does not support vibration.'); +		} else { +			navigator.vibrate(p_duration_ms); +		} +	}, +}; + +autoAddDeps(GodotInput, '$GodotInput'); +mergeInto(LibraryManager.library, GodotInput); diff --git a/platform/web/js/libs/library_godot_javascript_singleton.js b/platform/web/js/libs/library_godot_javascript_singleton.js new file mode 100644 index 0000000000..692f27676a --- /dev/null +++ b/platform/web/js/libs/library_godot_javascript_singleton.js @@ -0,0 +1,346 @@ +/*************************************************************************/ +/*  library_godot_eval.js                                                */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2022 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 GodotJSWrapper = { + +	$GodotJSWrapper__deps: ['$GodotRuntime', '$IDHandler'], +	$GodotJSWrapper__postset: 'GodotJSWrapper.proxies = new Map();', +	$GodotJSWrapper: { +		proxies: null, +		cb_ret: null, + +		MyProxy: function (val) { +			const id = IDHandler.add(this); +			GodotJSWrapper.proxies.set(val, id); +			let refs = 1; +			this.ref = function () { +				refs++; +			}; +			this.unref = function () { +				refs--; +				if (refs === 0) { +					IDHandler.remove(id); +					GodotJSWrapper.proxies.delete(val); +				} +			}; +			this.get_val = function () { +				return val; +			}; +			this.get_id = function () { +				return id; +			}; +		}, + +		get_proxied: function (val) { +			const id = GodotJSWrapper.proxies.get(val); +			if (id === undefined) { +				const proxy = new GodotJSWrapper.MyProxy(val); +				return proxy.get_id(); +			} +			IDHandler.get(id).ref(); +			return id; +		}, + +		get_proxied_value: function (id) { +			const proxy = IDHandler.get(id); +			if (proxy === undefined) { +				return undefined; +			} +			return proxy.get_val(); +		}, + +		variant2js: function (type, val) { +			switch (type) { +			case 0: +				return null; +			case 1: +				return !!GodotRuntime.getHeapValue(val, 'i64'); +			case 2: +				return GodotRuntime.getHeapValue(val, 'i64'); +			case 3: +				return GodotRuntime.getHeapValue(val, 'double'); +			case 4: +				return GodotRuntime.parseString(GodotRuntime.getHeapValue(val, '*')); +			case 21: // OBJECT +				return GodotJSWrapper.get_proxied_value(GodotRuntime.getHeapValue(val, 'i64')); +			default: +				return undefined; +			} +		}, + +		js2variant: function (p_val, p_exchange) { +			if (p_val === undefined || p_val === null) { +				return 0; // NIL +			} +			const type = typeof (p_val); +			if (type === 'boolean') { +				GodotRuntime.setHeapValue(p_exchange, p_val, 'i64'); +				return 1; // BOOL +			} else if (type === 'number') { +				if (Number.isInteger(p_val)) { +					GodotRuntime.setHeapValue(p_exchange, p_val, 'i64'); +					return 2; // INT +				} +				GodotRuntime.setHeapValue(p_exchange, p_val, 'double'); +				return 3; // REAL +			} else if (type === 'string') { +				const c_str = GodotRuntime.allocString(p_val); +				GodotRuntime.setHeapValue(p_exchange, c_str, '*'); +				return 4; // STRING +			} +			const id = GodotJSWrapper.get_proxied(p_val); +			GodotRuntime.setHeapValue(p_exchange, id, 'i64'); +			return 21; +		}, +	}, + +	godot_js_wrapper_interface_get__sig: 'ii', +	godot_js_wrapper_interface_get: function (p_name) { +		const name = GodotRuntime.parseString(p_name); +		if (typeof (window[name]) !== 'undefined') { +			return GodotJSWrapper.get_proxied(window[name]); +		} +		return 0; +	}, + +	godot_js_wrapper_object_get__sig: 'iiii', +	godot_js_wrapper_object_get: function (p_id, p_exchange, p_prop) { +		const obj = GodotJSWrapper.get_proxied_value(p_id); +		if (obj === undefined) { +			return 0; +		} +		if (p_prop) { +			const prop = GodotRuntime.parseString(p_prop); +			try { +				return GodotJSWrapper.js2variant(obj[prop], p_exchange); +			} catch (e) { +				GodotRuntime.error(`Error getting variable ${prop} on object`, obj); +				return 0; // NIL +			} +		} +		return GodotJSWrapper.js2variant(obj, p_exchange); +	}, + +	godot_js_wrapper_object_set__sig: 'viiii', +	godot_js_wrapper_object_set: function (p_id, p_name, p_type, p_exchange) { +		const obj = GodotJSWrapper.get_proxied_value(p_id); +		if (obj === undefined) { +			return; +		} +		const name = GodotRuntime.parseString(p_name); +		try { +			obj[name] = GodotJSWrapper.variant2js(p_type, p_exchange); +		} catch (e) { +			GodotRuntime.error(`Error setting variable ${name} on object`, obj); +		} +	}, + +	godot_js_wrapper_object_call__sig: 'iiiiiiiii', +	godot_js_wrapper_object_call: function (p_id, p_method, p_args, p_argc, p_convert_callback, p_exchange, p_lock, p_free_lock_callback) { +		const obj = GodotJSWrapper.get_proxied_value(p_id); +		if (obj === undefined) { +			return -1; +		} +		const method = GodotRuntime.parseString(p_method); +		const convert = GodotRuntime.get_func(p_convert_callback); +		const freeLock = GodotRuntime.get_func(p_free_lock_callback); +		const args = new Array(p_argc); +		for (let i = 0; i < p_argc; i++) { +			const type = convert(p_args, i, p_exchange, p_lock); +			const lock = GodotRuntime.getHeapValue(p_lock, '*'); +			args[i] = GodotJSWrapper.variant2js(type, p_exchange); +			if (lock) { +				freeLock(p_lock, type); +			} +		} +		try { +			const res = obj[method](...args); +			return GodotJSWrapper.js2variant(res, p_exchange); +		} catch (e) { +			GodotRuntime.error(`Error calling method ${method} on:`, obj, 'error:', e); +			return -1; +		} +	}, + +	godot_js_wrapper_object_unref__sig: 'vi', +	godot_js_wrapper_object_unref: function (p_id) { +		const proxy = IDHandler.get(p_id); +		if (proxy !== undefined) { +			proxy.unref(); +		} +	}, + +	godot_js_wrapper_create_cb__sig: 'iii', +	godot_js_wrapper_create_cb: function (p_ref, p_func) { +		const func = GodotRuntime.get_func(p_func); +		let id = 0; +		const cb = function () { +			if (!GodotJSWrapper.get_proxied_value(id)) { +				return undefined; +			} +			// The callback will store the returned value in this variable via +			// "godot_js_wrapper_object_set_cb_ret" upon calling the user function. +			// This is safe! JavaScript is single threaded (and using it in threads is not a good idea anyway). +			GodotJSWrapper.cb_ret = null; +			const args = Array.from(arguments); +			func(p_ref, GodotJSWrapper.get_proxied(args), args.length); +			const ret = GodotJSWrapper.cb_ret; +			GodotJSWrapper.cb_ret = null; +			return ret; +		}; +		id = GodotJSWrapper.get_proxied(cb); +		return id; +	}, + +	godot_js_wrapper_object_set_cb_ret__sig: 'vii', +	godot_js_wrapper_object_set_cb_ret: function (p_val_type, p_val_ex) { +		GodotJSWrapper.cb_ret = GodotJSWrapper.variant2js(p_val_type, p_val_ex); +	}, + +	godot_js_wrapper_object_getvar__sig: 'iiii', +	godot_js_wrapper_object_getvar: function (p_id, p_type, p_exchange) { +		const obj = GodotJSWrapper.get_proxied_value(p_id); +		if (obj === undefined) { +			return -1; +		} +		const prop = GodotJSWrapper.variant2js(p_type, p_exchange); +		if (prop === undefined || prop === null) { +			return -1; +		} +		try { +			return GodotJSWrapper.js2variant(obj[prop], p_exchange); +		} catch (e) { +			GodotRuntime.error(`Error getting variable ${prop} on object`, obj, e); +			return -1; +		} +	}, + +	godot_js_wrapper_object_setvar__sig: 'iiiiii', +	godot_js_wrapper_object_setvar: function (p_id, p_key_type, p_key_ex, p_val_type, p_val_ex) { +		const obj = GodotJSWrapper.get_proxied_value(p_id); +		if (obj === undefined) { +			return -1; +		} +		const key = GodotJSWrapper.variant2js(p_key_type, p_key_ex); +		try { +			obj[key] = GodotJSWrapper.variant2js(p_val_type, p_val_ex); +			return 0; +		} catch (e) { +			GodotRuntime.error(`Error setting variable ${key} on object`, obj); +			return -1; +		} +	}, + +	godot_js_wrapper_create_object__sig: 'iiiiiiii', +	godot_js_wrapper_create_object: function (p_object, p_args, p_argc, p_convert_callback, p_exchange, p_lock, p_free_lock_callback) { +		const name = GodotRuntime.parseString(p_object); +		if (typeof (window[name]) === 'undefined') { +			return -1; +		} +		const convert = GodotRuntime.get_func(p_convert_callback); +		const freeLock = GodotRuntime.get_func(p_free_lock_callback); +		const args = new Array(p_argc); +		for (let i = 0; i < p_argc; i++) { +			const type = convert(p_args, i, p_exchange, p_lock); +			const lock = GodotRuntime.getHeapValue(p_lock, '*'); +			args[i] = GodotJSWrapper.variant2js(type, p_exchange); +			if (lock) { +				freeLock(p_lock, type); +			} +		} +		try { +			const res = new window[name](...args); +			return GodotJSWrapper.js2variant(res, p_exchange); +		} catch (e) { +			GodotRuntime.error(`Error calling constructor ${name} with args:`, args, 'error:', e); +			return -1; +		} +	}, +}; + +autoAddDeps(GodotJSWrapper, '$GodotJSWrapper'); +mergeInto(LibraryManager.library, GodotJSWrapper); + +const GodotEval = { +	godot_js_eval__deps: ['$GodotRuntime'], +	godot_js_eval__sig: 'iiiiiii', +	godot_js_eval: function (p_js, p_use_global_ctx, p_union_ptr, p_byte_arr, p_byte_arr_write, p_callback) { +		const js_code = GodotRuntime.parseString(p_js); +		let eval_ret = null; +		try { +			if (p_use_global_ctx) { +				// indirect eval call grants global execution context +				const global_eval = eval; // eslint-disable-line no-eval +				eval_ret = global_eval(js_code); +			} else { +				eval_ret = eval(js_code); // eslint-disable-line no-eval +			} +		} catch (e) { +			GodotRuntime.error(e); +		} + +		switch (typeof eval_ret) { +		case 'boolean': +			GodotRuntime.setHeapValue(p_union_ptr, eval_ret, 'i32'); +			return 1; // BOOL + +		case 'number': +			GodotRuntime.setHeapValue(p_union_ptr, eval_ret, 'double'); +			return 3; // REAL + +		case 'string': +			GodotRuntime.setHeapValue(p_union_ptr, GodotRuntime.allocString(eval_ret), '*'); +			return 4; // STRING + +		case 'object': +			if (eval_ret === null) { +				break; +			} + +			if (ArrayBuffer.isView(eval_ret) && !(eval_ret instanceof Uint8Array)) { +				eval_ret = new Uint8Array(eval_ret.buffer); +			} else if (eval_ret instanceof ArrayBuffer) { +				eval_ret = new Uint8Array(eval_ret); +			} +			if (eval_ret instanceof Uint8Array) { +				const func = GodotRuntime.get_func(p_callback); +				const bytes_ptr = func(p_byte_arr, p_byte_arr_write, eval_ret.length); +				HEAPU8.set(eval_ret, bytes_ptr); +				return 20; // POOL_BYTE_ARRAY +			} +			break; + +			// no default +		} +		return 0; // NIL +	}, +}; + +mergeInto(LibraryManager.library, GodotEval); diff --git a/platform/web/js/libs/library_godot_os.js b/platform/web/js/libs/library_godot_os.js new file mode 100644 index 0000000000..377eec3234 --- /dev/null +++ b/platform/web/js/libs/library_godot_os.js @@ -0,0 +1,427 @@ +/*************************************************************************/ +/*  library_godot_os.js                                                  */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2022 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 IDHandler = { +	$IDHandler: { +		_last_id: 0, +		_references: {}, + +		get: function (p_id) { +			return IDHandler._references[p_id]; +		}, + +		add: function (p_data) { +			const id = ++IDHandler._last_id; +			IDHandler._references[id] = p_data; +			return id; +		}, + +		remove: function (p_id) { +			delete IDHandler._references[p_id]; +		}, +	}, +}; + +autoAddDeps(IDHandler, '$IDHandler'); +mergeInto(LibraryManager.library, IDHandler); + +const GodotConfig = { +	$GodotConfig__postset: 'Module["initConfig"] = GodotConfig.init_config;', +	$GodotConfig__deps: ['$GodotRuntime'], +	$GodotConfig: { +		canvas: null, +		locale: 'en', +		canvas_resize_policy: 2, // Adaptive +		virtual_keyboard: false, +		persistent_drops: false, +		on_execute: null, +		on_exit: null, + +		init_config: function (p_opts) { +			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']; +			GodotConfig.on_exit = p_opts['onExit']; +			if (p_opts['focusCanvas']) { +				GodotConfig.canvas.focus(); +			} +		}, + +		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', +	godot_js_config_canvas_id_get: function (p_ptr, p_ptr_max) { +		GodotRuntime.stringToHeap(`#${GodotConfig.canvas.id}`, p_ptr, p_ptr_max); +	}, + +	godot_js_config_locale_get__sig: 'vii', +	godot_js_config_locale_get: function (p_ptr, p_ptr_max) { +		GodotRuntime.stringToHeap(GodotConfig.locale, p_ptr, p_ptr_max); +	}, +}; + +autoAddDeps(GodotConfig, '$GodotConfig'); +mergeInto(LibraryManager.library, GodotConfig); + +const GodotFS = { +	$GodotFS__deps: ['$ERRNO_CODES', '$FS', '$IDBFS', '$GodotRuntime'], +	$GodotFS__postset: [ +		'Module["initFS"] = GodotFS.init;', +		'Module["copyToFS"] = GodotFS.copy_to_fs;', +	].join(''), +	$GodotFS: { +		_idbfs: false, +		_syncing: false, +		_mount_points: [], + +		is_persistent: function () { +			return GodotFS._idbfs ? 1 : 0; +		}, + +		// Initialize godot file system, setting up persistent paths. +		// Returns a promise that resolves when the FS is ready. +		// We keep track of mount_points, so that we can properly close the IDBFS +		// since emscripten is not doing it by itself. (emscripten GH#12516). +		init: function (persistentPaths) { +			GodotFS._idbfs = false; +			if (!Array.isArray(persistentPaths)) { +				return Promise.reject(new Error('Persistent paths must be an array')); +			} +			if (!persistentPaths.length) { +				return Promise.resolve(); +			} +			GodotFS._mount_points = persistentPaths.slice(); + +			function createRecursive(dir) { +				try { +					FS.stat(dir); +				} catch (e) { +					if (e.errno !== ERRNO_CODES.ENOENT) { +						throw e; +					} +					FS.mkdirTree(dir); +				} +			} + +			GodotFS._mount_points.forEach(function (path) { +				createRecursive(path); +				FS.mount(IDBFS, {}, path); +			}); +			return new Promise(function (resolve, reject) { +				FS.syncfs(true, function (err) { +					if (err) { +						GodotFS._mount_points = []; +						GodotFS._idbfs = false; +						GodotRuntime.print(`IndexedDB not available: ${err.message}`); +					} else { +						GodotFS._idbfs = true; +					} +					resolve(err); +				}); +			}); +		}, + +		// Deinit godot file system, making sure to unmount file systems, and close IDBFS(s). +		deinit: function () { +			GodotFS._mount_points.forEach(function (path) { +				try { +					FS.unmount(path); +				} catch (e) { +					GodotRuntime.print('Already unmounted', e); +				} +				if (GodotFS._idbfs && IDBFS.dbs[path]) { +					IDBFS.dbs[path].close(); +					delete IDBFS.dbs[path]; +				} +			}); +			GodotFS._mount_points = []; +			GodotFS._idbfs = false; +			GodotFS._syncing = false; +		}, + +		sync: function () { +			if (GodotFS._syncing) { +				GodotRuntime.error('Already syncing!'); +				return Promise.resolve(); +			} +			GodotFS._syncing = true; +			return new Promise(function (resolve, reject) { +				FS.syncfs(false, function (error) { +					if (error) { +						GodotRuntime.error(`Failed to save IDB file system: ${error.message}`); +					} +					GodotFS._syncing = false; +					resolve(error); +				}); +			}); +		}, + +		// Copies a buffer to the internal file system. Creating directories recursively. +		copy_to_fs: function (path, buffer) { +			const idx = path.lastIndexOf('/'); +			let dir = '/'; +			if (idx > 0) { +				dir = path.slice(0, idx); +			} +			try { +				FS.stat(dir); +			} catch (e) { +				if (e.errno !== ERRNO_CODES.ENOENT) { +					throw e; +				} +				FS.mkdirTree(dir); +			} +			FS.writeFile(path, new Uint8Array(buffer)); +		}, +	}, +}; +mergeInto(LibraryManager.library, GodotFS); + +const GodotOS = { +	$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: { +		request_quit: function () {}, +		_async_cbs: [], +		_fs_sync_promise: null, + +		atexit: function (p_promise_cb) { +			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 = []; +				GodotOS._async_cbs.forEach(function (cb) { +					promises.push(new Promise(cb)); +				}); +				return Promise.all(promises); +			}).then(function () { +				return GodotFS.sync(); // Final FS sync. +			}).then(function (err) { +				// Always deferred. +				setTimeout(function () { +					callback(); +				}, 0); +			}); +		}, +	}, + +	godot_js_os_finish_async__sig: 'vi', +	godot_js_os_finish_async: function (p_callback) { +		const func = GodotRuntime.get_func(p_callback); +		GodotOS.finish_async(func); +	}, + +	godot_js_os_request_quit_cb__sig: 'vi', +	godot_js_os_request_quit_cb: function (p_callback) { +		GodotOS.request_quit = GodotRuntime.get_func(p_callback); +	}, + +	godot_js_os_fs_is_persistent__sig: 'i', +	godot_js_os_fs_is_persistent: function () { +		return GodotFS.is_persistent(); +	}, + +	godot_js_os_fs_sync__sig: 'vi', +	godot_js_os_fs_sync: function (callback) { +		const func = GodotRuntime.get_func(callback); +		GodotOS._fs_sync_promise = GodotFS.sync(); +		GodotOS._fs_sync_promise.then(function (err) { +			func(); +		}); +	}, + +	godot_js_os_execute__sig: 'ii', +	godot_js_os_execute: function (p_json) { +		const json_args = GodotRuntime.parseString(p_json); +		const args = JSON.parse(json_args); +		if (GodotConfig.on_execute) { +			GodotConfig.on_execute(args); +			return 0; +		} +		return 1; +	}, + +	godot_js_os_shell_open__sig: 'vi', +	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 () { +		// TODO Godot core needs fixing to avoid spawning too many threads (> 24). +		const concurrency = navigator.hardwareConcurrency || 1; +		return concurrency < 2 ? concurrency : 2; +	}, + +	godot_js_os_download_buffer__sig: 'viiii', +	godot_js_os_download_buffer: function (p_ptr, p_size, p_name, p_mime) { +		const buf = GodotRuntime.heapSlice(HEAP8, p_ptr, p_size); +		const name = GodotRuntime.parseString(p_name); +		const mime = GodotRuntime.parseString(p_mime); +		const blob = new Blob([buf], { type: mime }); +		const url = window.URL.createObjectURL(blob); +		const a = document.createElement('a'); +		a.href = url; +		a.download = name; +		a.style.display = 'none'; +		document.body.appendChild(a); +		a.click(); +		a.remove(); +		window.URL.revokeObjectURL(url); +	}, +}; + +autoAddDeps(GodotOS, '$GodotOS'); +mergeInto(LibraryManager.library, GodotOS); + +/* + * Godot event listeners. + * Keeps track of registered event listeners so it can remove them on shutdown. + */ +const GodotEventListeners = { +	$GodotEventListeners__deps: ['$GodotOS'], +	$GodotEventListeners__postset: 'GodotOS.atexit(function(resolve, reject) { GodotEventListeners.clear(); resolve(); });', +	$GodotEventListeners: { +		handlers: [], + +		has: function (target, event, method, capture) { +			return GodotEventListeners.handlers.findIndex(function (e) { +				return e.target === target && e.event === event && e.method === method && e.capture === capture; +			}) !== -1; +		}, + +		add: function (target, event, method, capture) { +			if (GodotEventListeners.has(target, event, method, capture)) { +				return; +			} +			function Handler(p_target, p_event, p_method, p_capture) { +				this.target = p_target; +				this.event = p_event; +				this.method = p_method; +				this.capture = p_capture; +			} +			GodotEventListeners.handlers.push(new Handler(target, event, method, capture)); +			target.addEventListener(event, method, capture); +		}, + +		clear: function () { +			GodotEventListeners.handlers.forEach(function (h) { +				h.target.removeEventListener(h.event, h.method, h.capture); +			}); +			GodotEventListeners.handlers.length = 0; +		}, +	}, +}; +mergeInto(LibraryManager.library, GodotEventListeners); + +const GodotPWA = { + +	$GodotPWA__deps: ['$GodotRuntime', '$GodotEventListeners'], +	$GodotPWA: { +		hasUpdate: false, + +		updateState: function (cb, reg) { +			if (!reg) { +				return; +			} +			if (!reg.active) { +				return; +			} +			if (reg.waiting) { +				GodotPWA.hasUpdate = true; +				cb(); +			} +			GodotEventListeners.add(reg, 'updatefound', function () { +				const installing = reg.installing; +				GodotEventListeners.add(installing, 'statechange', function () { +					if (installing.state === 'installed') { +						GodotPWA.hasUpdate = true; +						cb(); +					} +				}); +			}); +		}, +	}, + +	godot_js_pwa_cb__sig: 'vi', +	godot_js_pwa_cb: function (p_update_cb) { +		if ('serviceWorker' in navigator) { +			const cb = GodotRuntime.get_func(p_update_cb); +			navigator.serviceWorker.getRegistration().then(GodotPWA.updateState.bind(null, cb)); +		} +	}, + +	godot_js_pwa_update__sig: 'i', +	godot_js_pwa_update: function () { +		if ('serviceWorker' in navigator && GodotPWA.hasUpdate) { +			navigator.serviceWorker.getRegistration().then(function (reg) { +				if (!reg || !reg.waiting) { +					return; +				} +				reg.waiting.postMessage('update'); +			}); +			return 0; +		} +		return 1; +	}, +}; + +autoAddDeps(GodotPWA, '$GodotPWA'); +mergeInto(LibraryManager.library, GodotPWA); diff --git a/platform/web/js/libs/library_godot_runtime.js b/platform/web/js/libs/library_godot_runtime.js new file mode 100644 index 0000000000..e2f7c8dca6 --- /dev/null +++ b/platform/web/js/libs/library_godot_runtime.js @@ -0,0 +1,134 @@ +/*************************************************************************/ +/*  library_godot_runtime.js                                             */ +/*************************************************************************/ +/*                       This file is part of:                           */ +/*                           GODOT ENGINE                                */ +/*                      https://godotengine.org                          */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */ +/* Copyright (c) 2014-2022 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 GodotRuntime = { +	$GodotRuntime: { +		/* +		 * Functions +		 */ +		get_func: function (ptr) { +			return wasmTable.get(ptr); // eslint-disable-line no-undef +		}, + +		/* +		 * Prints +		 */ +		error: function () { +			err.apply(null, Array.from(arguments)); // eslint-disable-line no-undef +		}, + +		print: function () { +			out.apply(null, Array.from(arguments)); // eslint-disable-line no-undef +		}, + +		/* +		 * Memory +		 */ +		malloc: function (p_size) { +			return _malloc(p_size); // eslint-disable-line no-undef +		}, + +		free: function (p_ptr) { +			_free(p_ptr); // eslint-disable-line no-undef +		}, + +		getHeapValue: function (p_ptr, p_type) { +			return getValue(p_ptr, p_type); // eslint-disable-line no-undef +		}, + +		setHeapValue: function (p_ptr, p_value, p_type) { +			setValue(p_ptr, p_value, p_type); // eslint-disable-line no-undef +		}, + +		heapSub: function (p_heap, p_ptr, p_len) { +			const bytes = p_heap.BYTES_PER_ELEMENT; +			return p_heap.subarray(p_ptr / bytes, p_ptr / bytes + 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 +		 */ +		parseString: function (p_ptr) { +			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 +		}, + +		allocString: function (p_str) { +			const length = GodotRuntime.strlen(p_str) + 1; +			const c_str = GodotRuntime.malloc(length); +			stringToUTF8(p_str, c_str, length); // eslint-disable-line no-undef +			return c_str; +		}, + +		allocStringArray: function (p_strings) { +			const size = p_strings.length; +			const c_ptr = GodotRuntime.malloc(size * 4); +			for (let i = 0; i < size; i++) { +				HEAP32[(c_ptr >> 2) + i] = GodotRuntime.allocString(p_strings[i]); +			} +			return c_ptr; +		}, + +		freeStringArray: function (p_ptr, p_len) { +			for (let i = 0; i < p_len; i++) { +				GodotRuntime.free(HEAP32[(p_ptr >> 2) + i]); +			} +			GodotRuntime.free(p_ptr); +		}, + +		stringToHeap: function (p_str, p_ptr, p_len) { +			return stringToUTF8Array(p_str, HEAP8, p_ptr, p_len); // eslint-disable-line no-undef +		}, +	}, +}; +autoAddDeps(GodotRuntime, '$GodotRuntime'); +mergeInto(LibraryManager.library, GodotRuntime);  |