summaryrefslogtreecommitdiff
path: root/platform/javascript/js
diff options
context:
space:
mode:
authorRĂ©mi Verschelde <rverschelde@gmail.com>2020-11-23 12:38:17 +0100
committerGitHub <noreply@github.com>2020-11-23 12:38:17 +0100
commit50db0e66ac8c00f176c706cee0c3a5469453acce (patch)
tree8ef45399f10c32b5dad742cf30180b569519cb70 /platform/javascript/js
parent18023cc3ede6fd84842db77019085c383e807016 (diff)
parent4617a7fa9cea323978f14053ef2726c84e9bd267 (diff)
Merge pull request #43747 from Faless/js/4.0_lint
[HTML5] Linting, fixes.
Diffstat (limited to 'platform/javascript/js')
-rw-r--r--platform/javascript/js/engine/engine.externs.js3
-rw-r--r--platform/javascript/js/engine/engine.js285
-rw-r--r--platform/javascript/js/engine/preloader.js127
-rw-r--r--platform/javascript/js/engine/utils.js56
-rw-r--r--platform/javascript/js/libs/audio.worklet.js186
-rw-r--r--platform/javascript/js/libs/library_godot_audio.js344
-rw-r--r--platform/javascript/js/libs/library_godot_display.js478
-rw-r--r--platform/javascript/js/libs/library_godot_editor_tools.js56
-rw-r--r--platform/javascript/js/libs/library_godot_eval.js85
-rw-r--r--platform/javascript/js/libs/library_godot_http_request.js145
-rw-r--r--platform/javascript/js/libs/library_godot_os.js279
-rw-r--r--platform/javascript/js/libs/library_godot_runtime.js120
12 files changed, 2164 insertions, 0 deletions
diff --git a/platform/javascript/js/engine/engine.externs.js b/platform/javascript/js/engine/engine.externs.js
new file mode 100644
index 0000000000..1a94dd15ec
--- /dev/null
+++ b/platform/javascript/js/engine/engine.externs.js
@@ -0,0 +1,3 @@
+var Godot;
+var WebAssembly = {};
+WebAssembly.instantiate = function(buffer, imports) {};
diff --git a/platform/javascript/js/engine/engine.js b/platform/javascript/js/engine/engine.js
new file mode 100644
index 0000000000..74153b672a
--- /dev/null
+++ b/platform/javascript/js/engine/engine.js
@@ -0,0 +1,285 @@
+const Engine = (function () {
+ const preloader = new Preloader();
+
+ let wasmExt = '.wasm';
+ let unloadAfterInit = true;
+ let loadPath = '';
+ let loadPromise = null;
+ let initPromise = null;
+ let stderr = null;
+ let stdout = null;
+ let progressFunc = null;
+
+ function load(basePath) {
+ if (loadPromise == null) {
+ loadPath = basePath;
+ loadPromise = preloader.loadPromise(basePath + wasmExt);
+ preloader.setProgressFunc(progressFunc);
+ requestAnimationFrame(preloader.animateProgress);
+ }
+ return loadPromise;
+ }
+
+ function unload() {
+ loadPromise = null;
+ }
+
+ /** @constructor */
+ function Engine() { // eslint-disable-line no-shadow
+ this.canvas = null;
+ this.executableName = '';
+ this.rtenv = null;
+ this.customLocale = null;
+ this.resizeCanvasOnStart = false;
+ this.onExecute = null;
+ this.onExit = null;
+ this.persistentPaths = ['/userfs'];
+ }
+
+ Engine.prototype.init = /** @param {string=} basePath */ function (basePath) {
+ if (initPromise) {
+ return initPromise;
+ }
+ if (loadPromise == null) {
+ if (!basePath) {
+ initPromise = Promise.reject(new Error('A base path must be provided when calling `init` and the engine is not loaded.'));
+ return initPromise;
+ }
+ load(basePath);
+ }
+ let config = {};
+ if (typeof stdout === 'function') {
+ config.print = stdout;
+ }
+ if (typeof stderr === 'function') {
+ config.printErr = stderr;
+ }
+ const me = this;
+ initPromise = new Promise(function (resolve, reject) {
+ config['locateFile'] = Utils.createLocateRewrite(loadPath);
+ config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise);
+ Godot(config).then(function (module) {
+ module['initFS'](me.persistentPaths).then(function (fs_err) {
+ me.rtenv = module;
+ if (unloadAfterInit) {
+ unload();
+ }
+ resolve();
+ config = null;
+ });
+ });
+ });
+ return initPromise;
+ };
+
+ /** @type {function(string, string):Object} */
+ Engine.prototype.preloadFile = function (file, path) {
+ return preloader.preload(file, path);
+ };
+
+ /** @type {function(...string):Object} */
+ Engine.prototype.start = function () {
+ // Start from arguments.
+ const args = [];
+ for (let i = 0; i < arguments.length; i++) {
+ args.push(arguments[i]);
+ }
+ const me = this;
+ return me.init().then(function () {
+ if (!me.rtenv) {
+ return Promise.reject(new Error('The engine must be initialized before it can be started'));
+ }
+
+ if (!(me.canvas instanceof HTMLCanvasElement)) {
+ me.canvas = Utils.findCanvas();
+ if (!me.canvas) {
+ return Promise.reject(new Error('No canvas found in page'));
+ }
+ }
+
+ // Canvas can grab focus on click, or key events won't work.
+ if (me.canvas.tabIndex < 0) {
+ me.canvas.tabIndex = 0;
+ }
+
+ // Disable right-click context menu.
+ me.canvas.addEventListener('contextmenu', function (ev) {
+ ev.preventDefault();
+ }, false);
+
+ // Until context restoration is implemented warn the user of context loss.
+ me.canvas.addEventListener('webglcontextlost', function (ev) {
+ alert('WebGL context lost, please reload the page'); // eslint-disable-line no-alert
+ ev.preventDefault();
+ }, false);
+
+ // Browser locale, or custom one if defined.
+ let locale = me.customLocale;
+ if (!locale) {
+ locale = navigator.languages ? navigator.languages[0] : navigator.language;
+ locale = locale.split('.')[0];
+ }
+ // Emscripten configuration.
+ me.rtenv['thisProgram'] = me.executableName;
+ me.rtenv['noExitRuntime'] = true;
+ // Godot configuration.
+ me.rtenv['initConfig']({
+ 'resizeCanvasOnStart': me.resizeCanvasOnStart,
+ 'canvas': me.canvas,
+ 'locale': locale,
+ 'onExecute': function (p_args) {
+ if (me.onExecute) {
+ me.onExecute(p_args);
+ return 0;
+ }
+ return 1;
+ },
+ 'onExit': function (p_code) {
+ me.rtenv['deinitFS']();
+ if (me.onExit) {
+ me.onExit(p_code);
+ }
+ me.rtenv = null;
+ },
+ });
+
+ 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'](args);
+ initPromise = null;
+ resolve();
+ });
+ });
+ };
+
+ Engine.prototype.startGame = function (execName, mainPack, extraArgs) {
+ // Start and init with execName as loadPath if not inited.
+ this.executableName = execName;
+ const me = this;
+ return Promise.all([
+ this.init(execName),
+ this.preloadFile(mainPack, mainPack),
+ ]).then(function () {
+ let args = ['--main-pack', mainPack];
+ if (extraArgs) {
+ args = args.concat(extraArgs);
+ }
+ return me.start.apply(me, args);
+ });
+ };
+
+ Engine.prototype.setWebAssemblyFilenameExtension = function (override) {
+ if (String(override).length === 0) {
+ throw new Error('Invalid WebAssembly filename extension override');
+ }
+ wasmExt = String(override);
+ };
+
+ Engine.prototype.setUnloadAfterInit = function (enabled) {
+ unloadAfterInit = enabled;
+ };
+
+ Engine.prototype.setCanvas = function (canvasElem) {
+ this.canvas = canvasElem;
+ };
+
+ Engine.prototype.setCanvasResizedOnStart = function (enabled) {
+ this.resizeCanvasOnStart = enabled;
+ };
+
+ Engine.prototype.setLocale = function (locale) {
+ this.customLocale = locale;
+ };
+
+ Engine.prototype.setExecutableName = function (newName) {
+ this.executableName = newName;
+ };
+
+ Engine.prototype.setProgressFunc = function (func) {
+ progressFunc = func;
+ };
+
+ Engine.prototype.setStdoutFunc = function (func) {
+ const print = function (text) {
+ let msg = text;
+ if (arguments.length > 1) {
+ msg = Array.prototype.slice.call(arguments).join(' ');
+ }
+ func(msg);
+ };
+ if (this.rtenv) {
+ this.rtenv.print = print;
+ }
+ stdout = print;
+ };
+
+ Engine.prototype.setStderrFunc = function (func) {
+ const printErr = function (text) {
+ let msg = text;
+ if (arguments.length > 1) {
+ msg = Array.prototype.slice.call(arguments).join(' ');
+ }
+ func(msg);
+ };
+ if (this.rtenv) {
+ this.rtenv.printErr = printErr;
+ }
+ stderr = printErr;
+ };
+
+ Engine.prototype.setOnExecute = function (onExecute) {
+ this.onExecute = onExecute;
+ };
+
+ Engine.prototype.setOnExit = function (onExit) {
+ this.onExit = onExit;
+ };
+
+ Engine.prototype.copyToFS = function (path, buffer) {
+ if (this.rtenv == null) {
+ throw new Error('Engine must be inited before copying files');
+ }
+ this.rtenv['copyToFS'](path, buffer);
+ };
+
+ Engine.prototype.setPersistentPaths = function (persistentPaths) {
+ this.persistentPaths = persistentPaths;
+ };
+
+ Engine.prototype.requestQuit = function () {
+ if (this.rtenv) {
+ this.rtenv['request_quit']();
+ }
+ };
+
+ // Closure compiler exported engine methods.
+ /** @export */
+ Engine['isWebGLAvailable'] = Utils.isWebGLAvailable;
+ Engine['load'] = load;
+ Engine['unload'] = unload;
+ Engine.prototype['init'] = Engine.prototype.init;
+ Engine.prototype['preloadFile'] = Engine.prototype.preloadFile;
+ Engine.prototype['start'] = Engine.prototype.start;
+ Engine.prototype['startGame'] = Engine.prototype.startGame;
+ Engine.prototype['setWebAssemblyFilenameExtension'] = Engine.prototype.setWebAssemblyFilenameExtension;
+ Engine.prototype['setUnloadAfterInit'] = Engine.prototype.setUnloadAfterInit;
+ Engine.prototype['setCanvas'] = Engine.prototype.setCanvas;
+ Engine.prototype['setCanvasResizedOnStart'] = Engine.prototype.setCanvasResizedOnStart;
+ Engine.prototype['setLocale'] = Engine.prototype.setLocale;
+ Engine.prototype['setExecutableName'] = Engine.prototype.setExecutableName;
+ Engine.prototype['setProgressFunc'] = Engine.prototype.setProgressFunc;
+ Engine.prototype['setStdoutFunc'] = Engine.prototype.setStdoutFunc;
+ Engine.prototype['setStderrFunc'] = Engine.prototype.setStderrFunc;
+ Engine.prototype['setOnExecute'] = Engine.prototype.setOnExecute;
+ Engine.prototype['setOnExit'] = Engine.prototype.setOnExit;
+ Engine.prototype['copyToFS'] = Engine.prototype.copyToFS;
+ Engine.prototype['setPersistentPaths'] = Engine.prototype.setPersistentPaths;
+ Engine.prototype['requestQuit'] = Engine.prototype.requestQuit;
+ return Engine;
+}());
+if (typeof window !== 'undefined') {
+ window['Engine'] = Engine;
+}
diff --git a/platform/javascript/js/engine/preloader.js b/platform/javascript/js/engine/preloader.js
new file mode 100644
index 0000000000..ec34fb93f2
--- /dev/null
+++ b/platform/javascript/js/engine/preloader.js
@@ -0,0 +1,127 @@
+const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars
+ const loadXHR = function (resolve, reject, file, tracker, attempts) {
+ const xhr = new XMLHttpRequest();
+ tracker[file] = {
+ total: 0,
+ loaded: 0,
+ final: false,
+ };
+ xhr.onerror = function () {
+ if (attempts <= 1) {
+ reject(new Error(`Failed loading file '${file}'`));
+ } else {
+ setTimeout(function () {
+ loadXHR(resolve, reject, file, tracker, attempts - 1);
+ }, 1000);
+ }
+ };
+ xhr.onabort = function () {
+ tracker[file].final = true;
+ reject(new Error(`Loading file '${file}' was aborted.`));
+ };
+ xhr.onloadstart = function (ev) {
+ tracker[file].total = ev.total;
+ tracker[file].loaded = ev.loaded;
+ };
+ xhr.onprogress = function (ev) {
+ tracker[file].loaded = ev.loaded;
+ tracker[file].total = ev.total;
+ };
+ xhr.onload = function () {
+ if (xhr.status >= 400) {
+ if (xhr.status < 500 || attempts <= 1) {
+ reject(new Error(`Failed loading file '${file}': ${xhr.statusText}`));
+ xhr.abort();
+ } else {
+ setTimeout(function () {
+ loadXHR(resolve, reject, file, tracker, attempts - 1);
+ }, 1000);
+ }
+ } else {
+ tracker[file].final = true;
+ resolve(xhr);
+ }
+ };
+ // Make request.
+ xhr.open('GET', file);
+ if (!file.endsWith('.js')) {
+ xhr.responseType = 'arraybuffer';
+ }
+ xhr.send();
+ };
+
+ 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.final) {
+ 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) {
+ return new Promise(function (resolve, reject) {
+ loadXHR(resolve, reject, file, loadingFiles, DOWNLOAD_ATTEMPTS_MAX);
+ });
+ };
+
+ this.preloadedFiles = [];
+ this.preload = function (pathOrBuffer, destPath) {
+ let buffer = null;
+ if (typeof pathOrBuffer === 'string') {
+ const me = this;
+ return this.loadPromise(pathOrBuffer).then(function (xhr) {
+ me.preloadedFiles.push({
+ path: destPath || pathOrBuffer,
+ buffer: xhr.response,
+ });
+ 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/javascript/js/engine/utils.js b/platform/javascript/js/engine/utils.js
new file mode 100644
index 0000000000..d0fca4e1cb
--- /dev/null
+++ b/platform/javascript/js/engine/utils.js
@@ -0,0 +1,56 @@
+const Utils = { // eslint-disable-line no-unused-vars
+
+ createLocateRewrite: function (execName) {
+ function rw(path) {
+ if (path.endsWith('.worker.js')) {
+ return `${execName}.worker.js`;
+ } else if (path.endsWith('.audio.worklet.js')) {
+ return `${execName}.audio.worklet.js`;
+ } else if (path.endsWith('.js')) {
+ return `${execName}.js`;
+ } else if (path.endsWith('.wasm')) {
+ return `${execName}.wasm`;
+ }
+ return path;
+ }
+ return rw;
+ },
+
+ createInstantiatePromise: function (wasmLoader) {
+ let loader = wasmLoader;
+ function instantiateWasm(imports, onSuccess) {
+ loader.then(function (xhr) {
+ WebAssembly.instantiate(xhr.response, imports).then(function (result) {
+ onSuccess(result['instance'], result['module']);
+ });
+ });
+ loader = null;
+ return {};
+ }
+
+ return instantiateWasm;
+ },
+
+ findCanvas: function () {
+ const nodes = document.getElementsByTagName('canvas');
+ if (nodes.length && nodes[0] instanceof HTMLCanvasElement) {
+ return nodes[0];
+ }
+ return null;
+ },
+
+ isWebGLAvailable: function (majorVersion = 1) {
+ let testContext = false;
+ try {
+ const testCanvas = document.createElement('canvas');
+ if (majorVersion === 1) {
+ testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl');
+ } else if (majorVersion === 2) {
+ testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2');
+ }
+ } catch (e) {
+ // Not available
+ }
+ return !!testContext;
+ },
+};
diff --git a/platform/javascript/js/libs/audio.worklet.js b/platform/javascript/js/libs/audio.worklet.js
new file mode 100644
index 0000000000..414dc37097
--- /dev/null
+++ b/platform/javascript/js/libs/audio.worklet.js
@@ -0,0 +1,186 @@
+/*************************************************************************/
+/* audio.worklet.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 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) {
+ this.buffer = p_buffer;
+ this.avail = p_state;
+ this.rpos = 0;
+ this.wpos = 0;
+ }
+
+ data_left() {
+ return Atomics.load(this.avail, 0);
+ }
+
+ 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;
+ }
+ output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+ this.rpos += to_write;
+ Atomics.add(this.avail, 0, -output.length);
+ Atomics.notify(this.avail, 0);
+ }
+
+ 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);
+ } else {
+ const high = p_buffer.subarray(0, to_write - mw);
+ const low = p_buffer.subarray(to_write - mw);
+ this.buffer.set(high, this.wpos);
+ this.buffer.set(low);
+ }
+ let diff = to_write;
+ if (this.wpos + diff >= this.buffer.length) {
+ diff -= this.buffer.length;
+ }
+ this.wpos += diff;
+ Atomics.add(this.avail, 0, to_write);
+ Atomics.notify(this.avail, 0);
+ }
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ 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() {
+ 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.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);
+ this.output = new RingBuffer(p_data[2], avail_out);
+ } else if (p_cmd === 'stop') {
+ this.runing = false;
+ this.output = null;
+ this.input = null;
+ }
+ }
+
+ 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.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);
+ } 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/javascript/js/libs/library_godot_audio.js b/platform/javascript/js/libs/library_godot_audio.js
new file mode 100644
index 0000000000..0c1f477f34
--- /dev/null
+++ b/platform/javascript/js/libs/library_godot_audio.js
@@ -0,0 +1,344 @@
+/*************************************************************************/
+/* library_godot_audio.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 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 ctx = new (window.AudioContext || window.webkitAudioContext)({
+ sampleRate: mix_rate,
+ // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance.
+ });
+ 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(); // Immeditately 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; // Already started.
+ }
+ function gotMediaInput(stream) {
+ GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream);
+ callback(GodotAudio.input);
+ }
+ if (navigator.mediaDevices.getUserMedia) {
+ navigator.mediaDevices.getUserMedia({
+ 'audio': true,
+ }).then(gotMediaInput, function (e) {
+ GodotRuntime.print(e);
+ });
+ } else {
+ if (!navigator.getUserMedia) {
+ navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+ }
+ navigator.getUserMedia({
+ 'audio': true,
+ }, gotMediaInput, function (e) {
+ GodotRuntime.print(e);
+ });
+ }
+ },
+
+ 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__proxy: 'sync',
+ godot_audio_is_available: function () {
+ if (!(window.AudioContext || window.webkitAudioContext)) {
+ return 0;
+ }
+ return 1;
+ },
+
+ 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);
+ return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate);
+ },
+
+ godot_audio_resume: function () {
+ if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') {
+ GodotAudio.ctx.resume();
+ }
+ },
+
+ godot_audio_capture_start__proxy: 'sync',
+ godot_audio_capture_start: function () {
+ if (GodotAudio.input) {
+ return; // Already started.
+ }
+ GodotAudio.create_input(function (input) {
+ input.connect(GodotAudio.driver.get_node());
+ });
+ },
+
+ godot_audio_capture_stop__proxy: 'sync',
+ 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,
+
+ 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);
+ };
+ });
+ },
+
+ get_node: function () {
+ return GodotAudioWorklet.worklet;
+ },
+
+ close: function () {
+ return new Promise(function (resolve, reject) {
+ GodotAudioWorklet.promise.then(function () {
+ GodotAudioWorklet.worklet.port.postMessage({
+ 'cmd': 'stop',
+ 'data': null,
+ });
+ GodotAudioWorklet.worklet.disconnect();
+ GodotAudioWorklet.worklet = null;
+ GodotAudioWorklet.promise = null;
+ resolve();
+ });
+ });
+ },
+ },
+
+ godot_audio_worklet_create: function (channels) {
+ GodotAudioWorklet.create(channels);
+ },
+
+ 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_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: function (p_state, p_idx, p_value) {
+ return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value);
+ },
+
+ 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: function (buffer_length, channel_count) {
+ return GodotAudioScript.create(buffer_length, channel_count);
+ },
+
+ 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/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js
new file mode 100644
index 0000000000..9651b48952
--- /dev/null
+++ b/platform/javascript/js/libs/library_godot_display.js
@@ -0,0 +1,478 @@
+/*************************************************************************/
+/* library_godot_display.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 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. */
+/*************************************************************************/
+
+/*
+ * Display Server listeners.
+ * Keeps track of registered event listeners so it can remove them on shutdown.
+ */
+const GodotDisplayListeners = {
+ $GodotDisplayListeners__deps: ['$GodotOS'],
+ $GodotDisplayListeners__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayListeners.clear(); resolve(); });',
+ $GodotDisplayListeners: {
+ handlers: [],
+
+ has: function (target, event, method, capture) {
+ return GodotDisplayListeners.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 (GodotDisplayListeners.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;
+ }
+ GodotDisplayListeners.handlers.push(new Handler(target, event, method, capture));
+ target.addEventListener(event, method, capture);
+ },
+
+ clear: function () {
+ GodotDisplayListeners.handlers.forEach(function (h) {
+ h.target.removeEventListener(h.event, h.method, h.capture);
+ });
+ GodotDisplayListeners.handlers.length = 0;
+ },
+ },
+};
+mergeInto(LibraryManager.library, GodotDisplayListeners);
+
+/*
+ * Drag and drop handler.
+ * 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 GodotDisplayDragDrop = {
+ $GodotDisplayDragDrop__deps: ['$FS', '$GodotFS'],
+ $GodotDisplayDragDrop: {
+ promises: [],
+ pending_files: [],
+
+ add_entry: function (entry) {
+ if (entry.isDirectory) {
+ GodotDisplayDragDrop.add_dir(entry);
+ } else if (entry.isFile) {
+ GodotDisplayDragDrop.add_file(entry);
+ } else {
+ GodotRuntime.error('Unrecognized entry...', entry);
+ }
+ },
+
+ add_dir: function (entry) {
+ GodotDisplayDragDrop.promises.push(new Promise(function (resolve, reject) {
+ const reader = entry.createReader();
+ reader.readEntries(function (entries) {
+ for (let i = 0; i < entries.length; i++) {
+ GodotDisplayDragDrop.add_entry(entries[i]);
+ }
+ resolve();
+ });
+ }));
+ },
+
+ add_file: function (entry) {
+ GodotDisplayDragDrop.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'];
+ }
+ GodotDisplayDragDrop.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 (GodotDisplayDragDrop.promises.length === 0) {
+ resolve();
+ return;
+ }
+ GodotDisplayDragDrop.promises.pop().then(function () {
+ setTimeout(function () {
+ GodotDisplayDragDrop.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) {
+ GodotDisplayDragDrop.add_entry(entry);
+ }
+ }
+ } else {
+ GodotRuntime.error('File upload not supported');
+ }
+ new Promise(GodotDisplayDragDrop.process).then(function () {
+ const DROP = `/tmp/drop-${parseInt(Math.random() * (1 << 30), 10)}/`;
+ const drops = [];
+ const files = [];
+ FS.mkdir(DROP);
+ GodotDisplayDragDrop.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);
+ });
+ GodotDisplayDragDrop.promises = [];
+ GodotDisplayDragDrop.pending_files = [];
+ callback(drops);
+ const dirs = [DROP.substr(0, DROP.length - 1)];
+ // Remove temporary files
+ files.forEach(function (file) {
+ FS.unlink(file);
+ let dir = file.replace(DROP, '');
+ let idx = dir.lastIndexOf('/');
+ while (idx > 0) {
+ dir = dir.substr(0, idx);
+ if (dirs.indexOf(DROP + dir) === -1) {
+ dirs.push(DROP + dir);
+ }
+ idx = dir.lastIndexOf('/');
+ }
+ });
+ // Remove dirs.
+ dirs.sort(function (a, b) {
+ const al = (a.match(/\//g) || []).length;
+ const bl = (b.match(/\//g) || []).length;
+ if (al > bl) {
+ return -1;
+ } else if (al < bl) {
+ return 1;
+ }
+ return 0;
+ }).forEach(function (dir) {
+ FS.rmdir(dir);
+ });
+ });
+ },
+
+ handler: function (callback) {
+ return function (ev) {
+ GodotDisplayDragDrop._process_event(ev, callback);
+ };
+ },
+ },
+};
+mergeInto(LibraryManager.library, GodotDisplayDragDrop);
+
+/*
+ * 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];
+ });
+ },
+ },
+};
+mergeInto(LibraryManager.library, GodotDisplayCursor);
+
+/**
+ * Display server interface.
+ *
+ * Exposes all the functions needed by DisplayServer implementation.
+ */
+const GodotDisplay = {
+ $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop'],
+ $GodotDisplay: {
+ window_icon: '',
+ },
+
+ 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_display_alert: function (p_text) {
+ window.alert(GodotRuntime.parseString(p_text)); // eslint-disable-line no-alert
+ },
+
+ godot_js_display_pixel_ratio_get: function () {
+ return window.devicePixelRatio || 1;
+ },
+
+ /*
+ * Canvas
+ */
+ godot_js_display_canvas_focus: function () {
+ GodotConfig.canvas.focus();
+ },
+
+ godot_js_display_canvas_is_focused: function () {
+ return document.activeElement === GodotConfig.canvas;
+ },
+
+ godot_js_display_canvas_bounding_rect_position_get: function (r_x, r_y) {
+ const brect = GodotConfig.canvas.getBoundingClientRect();
+ GodotRuntime.setHeapValue(r_x, brect.x, 'i32');
+ GodotRuntime.setHeapValue(r_y, brect.y, 'i32');
+ },
+
+ /*
+ * Touchscreen
+ */
+ godot_js_display_touchscreen_is_available: function () {
+ return 'ontouchstart' in window;
+ },
+
+ /*
+ * Clipboard
+ */
+ 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 HTML5 plafrom. Exception:', e);
+ });
+ return 0;
+ },
+
+ 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_request_fullscreen: function () {
+ const canvas = GodotConfig.canvas;
+ (canvas.requestFullscreen || canvas.msRequestFullscreen
+ || canvas.mozRequestFullScreen || canvas.mozRequestFullscreen
+ || canvas.webkitRequestFullscreen
+ ).call(canvas);
+ },
+
+ godot_js_display_window_title_set: function (p_data) {
+ document.title = GodotRuntime.parseString(p_data);
+ },
+
+ 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.heapCopy(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: 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: function () {
+ return !GodotDisplayCursor.visible;
+ },
+
+ godot_js_display_cursor_set_shape: function (p_string) {
+ GodotDisplayCursor.set_shape(GodotRuntime.parseString(p_string));
+ },
+
+ 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.heapCopy(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);
+ }
+ },
+
+ /*
+ * Listeners
+ */
+ 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) {
+ GodotDisplayListeners.add(canvas, evt_name, function () {
+ func.bind(null, notif[idx]);
+ }, true);
+ });
+ },
+
+ godot_js_display_paste_cb: function (callback) {
+ const func = GodotRuntime.get_func(callback);
+ GodotDisplayListeners.add(window, 'paste', function (evt) {
+ const text = evt.clipboardData.getData('text');
+ const ptr = GodotRuntime.allocString(text);
+ func(ptr);
+ GodotRuntime.free(ptr);
+ }, false);
+ },
+
+ godot_js_display_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;
+ GodotDisplayListeners.add(canvas, 'dragover', function (ev) {
+ // Prevent default behavior (which would try to open the file(s))
+ ev.preventDefault();
+ }, false);
+ GodotDisplayListeners.add(canvas, 'drop', GodotDisplayDragDrop.handler(dropFiles));
+ },
+};
+
+autoAddDeps(GodotDisplay, '$GodotDisplay');
+mergeInto(LibraryManager.library, GodotDisplay);
diff --git a/platform/javascript/js/libs/library_godot_editor_tools.js b/platform/javascript/js/libs/library_godot_editor_tools.js
new file mode 100644
index 0000000000..f39fed04a8
--- /dev/null
+++ b/platform/javascript/js/libs/library_godot_editor_tools.js
@@ -0,0 +1,56 @@
+/*************************************************************************/
+/* library_godot_editor_tools.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 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 GodotEditorTools = {
+ godot_js_editor_download_file__deps: ['$FS'],
+ godot_js_editor_download_file: function (p_path, p_name, p_mime) {
+ const path = GodotRuntime.parseString(p_path);
+ const name = GodotRuntime.parseString(p_name);
+ const mime = GodotRuntime.parseString(p_mime);
+ const size = FS.stat(path)['size'];
+ const buf = new Uint8Array(size);
+ const fd = FS.open(path, 'r');
+ FS.read(fd, buf, 0, size);
+ FS.close(fd);
+ FS.unlink(path);
+ 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);
+ },
+};
+
+mergeInto(LibraryManager.library, GodotEditorTools);
diff --git a/platform/javascript/js/libs/library_godot_eval.js b/platform/javascript/js/libs/library_godot_eval.js
new file mode 100644
index 0000000000..33ff231726
--- /dev/null
+++ b/platform/javascript/js/libs/library_godot_eval.js
@@ -0,0 +1,85 @@
+/*************************************************************************/
+/* library_godot_eval.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 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 GodotEval = {
+ godot_js_eval__deps: ['$GodotRuntime'],
+ 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/javascript/js/libs/library_godot_http_request.js b/platform/javascript/js/libs/library_godot_http_request.js
new file mode 100644
index 0000000000..2b9aa88208
--- /dev/null
+++ b/platform/javascript/js/libs/library_godot_http_request.js
@@ -0,0 +1,145 @@
+/*************************************************************************/
+/* http_request.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+const GodotHTTPRequest = {
+ $GodotHTTPRequest__deps: ['$GodotRuntime'],
+ $GodotHTTPRequest: {
+ requests: [],
+
+ getUnusedRequestId: function () {
+ const idMax = GodotHTTPRequest.requests.length;
+ for (let potentialId = 0; potentialId < idMax; ++potentialId) {
+ if (GodotHTTPRequest.requests[potentialId] instanceof XMLHttpRequest) {
+ continue;
+ }
+ return potentialId;
+ }
+ GodotHTTPRequest.requests.push(null);
+ return idMax;
+ },
+
+ setupRequest: function (xhr) {
+ xhr.responseType = 'arraybuffer';
+ },
+ },
+
+ godot_xhr_new: function () {
+ const newId = GodotHTTPRequest.getUnusedRequestId();
+ GodotHTTPRequest.requests[newId] = new XMLHttpRequest();
+ GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[newId]);
+ return newId;
+ },
+
+ godot_xhr_reset: function (xhrId) {
+ GodotHTTPRequest.requests[xhrId] = new XMLHttpRequest();
+ GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[xhrId]);
+ },
+
+ godot_xhr_free: function (xhrId) {
+ GodotHTTPRequest.requests[xhrId].abort();
+ GodotHTTPRequest.requests[xhrId] = null;
+ },
+
+ godot_xhr_open: function (xhrId, method, url, p_user, p_password) {
+ const user = p_user > 0 ? GodotRuntime.parseString(p_user) : null;
+ const password = p_password > 0 ? GodotRuntime.parseString(p_password) : null;
+ GodotHTTPRequest.requests[xhrId].open(GodotRuntime.parseString(method), GodotRuntime.parseString(url), true, user, password);
+ },
+
+ godot_xhr_set_request_header: function (xhrId, header, value) {
+ GodotHTTPRequest.requests[xhrId].setRequestHeader(GodotRuntime.parseString(header), GodotRuntime.parseString(value));
+ },
+
+ godot_xhr_send_null: function (xhrId) {
+ GodotHTTPRequest.requests[xhrId].send();
+ },
+
+ godot_xhr_send_string: function (xhrId, strPtr) {
+ if (!strPtr) {
+ GodotRuntime.error('Failed to send string per XHR: null pointer');
+ return;
+ }
+ GodotHTTPRequest.requests[xhrId].send(GodotRuntime.parseString(strPtr));
+ },
+
+ godot_xhr_send_data: function (xhrId, ptr, len) {
+ if (!ptr) {
+ GodotRuntime.error('Failed to send data per XHR: null pointer');
+ return;
+ }
+ if (len < 0) {
+ GodotRuntime.error('Failed to send data per XHR: buffer length less than 0');
+ return;
+ }
+ GodotHTTPRequest.requests[xhrId].send(HEAPU8.subarray(ptr, ptr + len));
+ },
+
+ godot_xhr_abort: function (xhrId) {
+ GodotHTTPRequest.requests[xhrId].abort();
+ },
+
+ godot_xhr_get_status: function (xhrId) {
+ return GodotHTTPRequest.requests[xhrId].status;
+ },
+
+ godot_xhr_get_ready_state: function (xhrId) {
+ return GodotHTTPRequest.requests[xhrId].readyState;
+ },
+
+ godot_xhr_get_response_headers_length: function (xhrId) {
+ const headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders();
+ return headers === null ? 0 : GodotRuntime.strlen(headers);
+ },
+
+ godot_xhr_get_response_headers: function (xhrId, dst, len) {
+ const str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders();
+ if (str === null) {
+ return;
+ }
+ GodotRuntime.stringToHeap(str, dst, len);
+ },
+
+ godot_xhr_get_response_length: function (xhrId) {
+ const body = GodotHTTPRequest.requests[xhrId].response;
+ return body === null ? 0 : body.byteLength;
+ },
+
+ godot_xhr_get_response: function (xhrId, dst, len) {
+ let buf = GodotHTTPRequest.requests[xhrId].response;
+ if (buf === null) {
+ return;
+ }
+ buf = new Uint8Array(buf).subarray(0, len);
+ HEAPU8.set(buf, dst);
+ },
+};
+
+autoAddDeps(GodotHTTPRequest, '$GodotHTTPRequest');
+mergeInto(LibraryManager.library, GodotHTTPRequest);
diff --git a/platform/javascript/js/libs/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js
new file mode 100644
index 0000000000..488753d704
--- /dev/null
+++ b/platform/javascript/js/libs/library_godot_os.js
@@ -0,0 +1,279 @@
+/*************************************************************************/
+/* library_godot_os.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 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',
+ resize_on_start: false,
+ on_execute: null,
+
+ init_config: function (p_opts) {
+ GodotConfig.resize_on_start = !!p_opts['resizeCanvasOnStart'];
+ GodotConfig.canvas = p_opts['canvas'];
+ GodotConfig.locale = p_opts['locale'] || GodotConfig.locale;
+ GodotConfig.on_execute = p_opts['onExecute'];
+ // This is called by emscripten, even if undocumented.
+ Module['onExit'] = p_opts['onExit']; // eslint-disable-line no-undef
+ },
+
+ locate_file: function (file) {
+ return Module['locateFile'](file); // eslint-disable-line no-undef
+ },
+ },
+
+ 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: function (p_ptr, p_ptr_max) {
+ GodotRuntime.stringToHeap(GodotConfig.locale, p_ptr, p_ptr_max);
+ },
+
+ godot_js_config_is_resize_on_start: function () {
+ return GodotConfig.resize_on_start ? 1 : 0;
+ },
+};
+
+autoAddDeps(GodotConfig, '$GodotConfig');
+mergeInto(LibraryManager.library, GodotConfig);
+
+const GodotFS = {
+ $GodotFS__deps: ['$FS', '$IDBFS', '$GodotRuntime'],
+ $GodotFS__postset: [
+ 'Module["initFS"] = GodotFS.init;',
+ 'Module["deinitFS"] = GodotFS.deinit;',
+ 'Module["copyToFS"] = GodotFS.copy_to_fs;',
+ ].join(''),
+ $GodotFS: {
+ _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), { 'flags': 'wx+' });
+ },
+ },
+};
+mergeInto(LibraryManager.library, GodotFS);
+
+const GodotOS = {
+ $GodotOS__deps: ['$GodotFS', '$GodotRuntime'],
+ $GodotOS__postset: [
+ 'Module["request_quit"] = function() { GodotOS.request_quit() };',
+ '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);
+ },
+
+ 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: function (p_callback) {
+ const func = GodotRuntime.get_func(p_callback);
+ GodotOS.finish_async(func);
+ },
+
+ godot_js_os_request_quit_cb: function (p_callback) {
+ GodotOS.request_quit = GodotRuntime.get_func(p_callback);
+ },
+
+ godot_js_os_fs_is_persistent: function () {
+ return GodotFS.is_persistent();
+ },
+
+ 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: 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: function (p_uri) {
+ window.open(GodotRuntime.parseString(p_uri), '_blank');
+ },
+};
+
+autoAddDeps(GodotOS, '$GodotOS');
+mergeInto(LibraryManager.library, GodotOS);
diff --git a/platform/javascript/js/libs/library_godot_runtime.js b/platform/javascript/js/libs/library_godot_runtime.js
new file mode 100644
index 0000000000..04f29ad681
--- /dev/null
+++ b/platform/javascript/js/libs/library_godot_runtime.js
@@ -0,0 +1,120 @@
+/*************************************************************************/
+/* library_godot_runtime.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 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);
+ },
+
+ heapCopy: 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);
+ },
+
+ /*
+ * Strings
+ */
+ parseString: function (p_ptr) {
+ return UTF8ToString(p_ptr); // eslint-disable-line no-undef
+ },
+
+ 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);