From a82f70ea9f98ed0a602a20f7fe8954f6e4200b25 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Thu, 19 Nov 2020 16:54:07 +0100 Subject: [HTML5] Libraries refactor for linting. Initial work to make liniting easier. This includes: - Rename http_request.js to library_godot_http_request.js. - Rename externs.js to engine.externs.js. - New library_godot_runtime.js (GodotRuntime) wraps around emscripten functions. - Refactor of XMLHttpRequest handler in engine/preloader.js. - Few fixes to bugs spotted by early stage linting. --- platform/javascript/SCsub | 23 +- platform/javascript/engine/engine.js | 271 ------------ platform/javascript/engine/externs.js | 3 - platform/javascript/engine/preloader.js | 135 ------ platform/javascript/engine/utils.js | 51 --- platform/javascript/js/engine/engine.externs.js | 3 + platform/javascript/js/engine/engine.js | 278 ++++++++++++ platform/javascript/js/engine/preloader.js | 129 ++++++ platform/javascript/js/engine/utils.js | 57 +++ platform/javascript/js/libs/audio.worklet.js | 186 ++++++++ platform/javascript/js/libs/library_godot_audio.js | 340 +++++++++++++++ .../javascript/js/libs/library_godot_display.js | 477 +++++++++++++++++++++ .../js/libs/library_godot_editor_tools.js | 56 +++ platform/javascript/js/libs/library_godot_eval.js | 87 ++++ .../js/libs/library_godot_http_request.js | 143 ++++++ platform/javascript/js/libs/library_godot_os.js | 280 ++++++++++++ .../javascript/js/libs/library_godot_runtime.js | 120 ++++++ platform/javascript/native/audio.worklet.js | 185 -------- platform/javascript/native/http_request.js | 144 ------- platform/javascript/native/library_godot_audio.js | 338 --------------- .../javascript/native/library_godot_display.js | 477 --------------------- .../native/library_godot_editor_tools.js | 56 --- platform/javascript/native/library_godot_eval.js | 85 ---- platform/javascript/native/library_godot_os.js | 311 -------------- 24 files changed, 2168 insertions(+), 2067 deletions(-) delete mode 100644 platform/javascript/engine/engine.js delete mode 100644 platform/javascript/engine/externs.js delete mode 100644 platform/javascript/engine/preloader.js delete mode 100644 platform/javascript/engine/utils.js create mode 100644 platform/javascript/js/engine/engine.externs.js create mode 100644 platform/javascript/js/engine/engine.js create mode 100644 platform/javascript/js/engine/preloader.js create mode 100644 platform/javascript/js/engine/utils.js create mode 100644 platform/javascript/js/libs/audio.worklet.js create mode 100644 platform/javascript/js/libs/library_godot_audio.js create mode 100644 platform/javascript/js/libs/library_godot_display.js create mode 100644 platform/javascript/js/libs/library_godot_editor_tools.js create mode 100644 platform/javascript/js/libs/library_godot_eval.js create mode 100644 platform/javascript/js/libs/library_godot_http_request.js create mode 100644 platform/javascript/js/libs/library_godot_os.js create mode 100644 platform/javascript/js/libs/library_godot_runtime.js delete mode 100644 platform/javascript/native/audio.worklet.js delete mode 100644 platform/javascript/native/http_request.js delete mode 100644 platform/javascript/native/library_godot_audio.js delete mode 100644 platform/javascript/native/library_godot_display.js delete mode 100644 platform/javascript/native/library_godot_editor_tools.js delete mode 100644 platform/javascript/native/library_godot_eval.js delete mode 100644 platform/javascript/native/library_godot_os.js (limited to 'platform') diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index a0e6fa0e18..627ae778b1 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -20,27 +20,28 @@ build = env.add_program(build_targets, javascript_files) env.AddJSLibraries( [ - "native/http_request.js", - "native/library_godot_audio.js", - "native/library_godot_display.js", - "native/library_godot_os.js", + "js/libs/library_godot_audio.js", + "js/libs/library_godot_display.js", + "js/libs/library_godot_http_request.js", + "js/libs/library_godot_os.js", + "js/libs/library_godot_runtime.js", ] ) if env["tools"]: - env.AddJSLibraries(["native/library_godot_editor_tools.js"]) + env.AddJSLibraries(["js/libs/library_godot_editor_tools.js"]) if env["javascript_eval"]: - env.AddJSLibraries(["native/library_godot_eval.js"]) + env.AddJSLibraries(["js/libs/library_godot_eval.js"]) for lib in env["JS_LIBS"]: env.Append(LINKFLAGS=["--js-library", lib]) env.Depends(build, env["JS_LIBS"]) engine = [ - "engine/preloader.js", - "engine/utils.js", - "engine/engine.js", + "js/engine/preloader.js", + "js/engine/utils.js", + "js/engine/engine.js", ] -externs = [env.File("#platform/javascript/engine/externs.js")] +externs = [env.File("#platform/javascript/js/engine/engine.externs.js")] js_engine = env.CreateEngineFile("#bin/godot${PROGSUFFIX}.engine.js", engine, externs) env.Depends(js_engine, externs) @@ -59,7 +60,7 @@ out_files = [ zip_dir.File(binary_name + ".audio.worklet.js"), ] html_file = "#misc/dist/html/editor.html" if env["tools"] else "#misc/dist/html/full-size.html" -in_files = [js_wrapped, build[1], html_file, "#platform/javascript/native/audio.worklet.js"] +in_files = [js_wrapped, build[1], html_file, "#platform/javascript/js/libs/audio.worklet.js"] if env["threads_enabled"]: in_files.append(build[2]) out_files.append(zip_dir.File(binary_name + ".worker.js")) diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js deleted file mode 100644 index 3745e04479..0000000000 --- a/platform/javascript/engine/engine.js +++ /dev/null @@ -1,271 +0,0 @@ -Function('return this')()['Engine'] = (function() { - var preloader = new Preloader(); - - var wasmExt = '.wasm'; - var unloadAfterInit = true; - var loadPath = ''; - var loadPromise = null; - var initPromise = null; - var stderr = null; - var stdout = null; - var 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() { - 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); - } - var config = {}; - if (typeof stdout === 'function') - config.print = stdout; - if (typeof stderr === 'function') - config.printErr = stderr; - var 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. - var args = []; - for (var i = 0; i < arguments.length; i++) { - args.push(arguments[i]); - } - var 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(); - } - - // 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"); - ev.preventDefault(); - }, false); - - // Browser locale, or custom one if defined. - var 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; - var me = this; - return Promise.all([ - this.init(execName), - this.preloadFile(mainPack, mainPack) - ]).then(function() { - var 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) { - var print = function(text) { - if (arguments.length > 1) { - text = Array.prototype.slice.call(arguments).join(" "); - } - func(text); - }; - if (this.rtenv) - this.rtenv.print = print; - stdout = print; - }; - - Engine.prototype.setStderrFunc = function(func) { - var printErr = function(text) { - if (arguments.length > 1) - text = Array.prototype.slice.call(arguments).join(" "); - func(text); - }; - 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; -})(); diff --git a/platform/javascript/engine/externs.js b/platform/javascript/engine/externs.js deleted file mode 100644 index 1a94dd15ec..0000000000 --- a/platform/javascript/engine/externs.js +++ /dev/null @@ -1,3 +0,0 @@ -var Godot; -var WebAssembly = {}; -WebAssembly.instantiate = function(buffer, imports) {}; diff --git a/platform/javascript/engine/preloader.js b/platform/javascript/engine/preloader.js deleted file mode 100644 index b3467d009f..0000000000 --- a/platform/javascript/engine/preloader.js +++ /dev/null @@ -1,135 +0,0 @@ -var Preloader = /** @constructor */ function() { - var DOWNLOAD_ATTEMPTS_MAX = 4; - var progressFunc = null; - var lastProgress = { loaded: 0, total: 0 }; - - var loadingFiles = {}; - this.preloadedFiles = []; - - function loadXHR(resolve, reject, file, tracker) { - var xhr = new XMLHttpRequest; - xhr.open('GET', file); - if (!file.endsWith('.js')) { - xhr.responseType = 'arraybuffer'; - } - ['loadstart', 'progress', 'load', 'error', 'abort'].forEach(function(ev) { - xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker)); - }); - xhr.send(); - } - - function onXHREvent(resolve, reject, file, tracker, ev) { - if (this.status >= 400) { - if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { - reject(new Error("Failed loading file '" + file + "': " + this.statusText)); - this.abort(); - return; - } else { - setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000); - } - } - - switch (ev.type) { - case 'loadstart': - if (tracker[file] === undefined) { - tracker[file] = { - total: ev.total, - loaded: ev.loaded, - attempts: 0, - final: false, - }; - } - break; - - case 'progress': - tracker[file].loaded = ev.loaded; - tracker[file].total = ev.total; - break; - - case 'load': - tracker[file].final = true; - resolve(this); - break; - - case 'error': - if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) { - tracker[file].final = true; - reject(new Error("Failed loading file '" + file + "'")); - } else { - setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000); - } - break; - - case 'abort': - tracker[file].final = true; - reject(new Error("Loading file '" + file + "' was aborted.")); - break; - } - } - - this.loadPromise = function(file) { - return new Promise(function(resolve, reject) { - loadXHR(resolve, reject, file, loadingFiles); - }); - } - - this.preload = function(pathOrBuffer, destPath) { - if (pathOrBuffer instanceof ArrayBuffer) { - pathOrBuffer = new Uint8Array(pathOrBuffer); - } else if (ArrayBuffer.isView(pathOrBuffer)) { - pathOrBuffer = new Uint8Array(pathOrBuffer.buffer); - } - if (pathOrBuffer instanceof Uint8Array) { - this.preloadedFiles.push({ - path: destPath, - buffer: pathOrBuffer - }); - return Promise.resolve(); - } else if (typeof pathOrBuffer === 'string') { - var me = this; - return this.loadPromise(pathOrBuffer).then(function(xhr) { - me.preloadedFiles.push({ - path: destPath || pathOrBuffer, - buffer: xhr.response - }); - return Promise.resolve(); - }); - } else { - throw Promise.reject("Invalid object for preloading"); - } - }; - - var animateProgress = function() { - var loaded = 0; - var total = 0; - var totalIsValid = true; - var 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; // Also exposed to start it. - - this.setProgressFunc = function(callback) { - progressFunc = callback; - } -}; diff --git a/platform/javascript/engine/utils.js b/platform/javascript/engine/utils.js deleted file mode 100644 index 8455739a25..0000000000 --- a/platform/javascript/engine/utils.js +++ /dev/null @@ -1,51 +0,0 @@ -var Utils = { - 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 rw; - }, - - createInstantiatePromise: function(wasmLoader) { - function instantiateWasm(imports, onSuccess) { - wasmLoader.then(function(xhr) { - WebAssembly.instantiate(xhr.response, imports).then(function(result) { - onSuccess(result['instance'], result['module']); - }); - }); - wasmLoader = null; - return {}; - }; - - return instantiateWasm; - }, - - findCanvas: function() { - var nodes = document.getElementsByTagName('canvas'); - if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { - return nodes[0]; - } - throw new Error("No canvas found"); - }, - - isWebGLAvailable: function(majorVersion = 1) { - var testContext = false; - try { - var 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) {} - return !!testContext; - } -}; 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..792ec9cb28 --- /dev/null +++ b/platform/javascript/js/engine/engine.js @@ -0,0 +1,278 @@ +const Engine = (function() { + var preloader = new Preloader(); + + var wasmExt = '.wasm'; + var unloadAfterInit = true; + var loadPath = ''; + var loadPromise = null; + var initPromise = null; + var stderr = null; + var stdout = null; + var 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); + } + var config = {}; + if (typeof stdout === 'function') + config.print = stdout; + if (typeof stderr === 'function') + config.printErr = stderr; + var 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. + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + var 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. + var 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; + var me = this; + return Promise.all([ + this.init(execName), + this.preloadFile(mainPack, mainPack) + ]).then(function() { + var 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) { + var 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) { + var 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..8641646e6e --- /dev/null +++ b/platform/javascript/js/engine/preloader.js @@ -0,0 +1,129 @@ +var 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() { + + var loaded = 0; + var total = 0; + var totalIsValid = true; + var 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') { + var 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(); + } else { + 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..fbab9ba9f9 --- /dev/null +++ b/platform/javascript/js/engine/utils.js @@ -0,0 +1,57 @@ +var 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() { + var nodes = document.getElementsByTagName('canvas'); + if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { + return nodes[0]; + } + return null; + }, + + isWebGLAvailable: function(majorVersion = 1) { + + var testContext = false; + try { + var 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..a27035ef22 --- /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..a657d0a125 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_audio.js @@ -0,0 +1,340 @@ +/*************************************************************************/ +/* 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..28f63ba557 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_display.js @@ -0,0 +1,477 @@ +/*************************************************************************/ +/* 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..21e40185ae --- /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..4064938d3e --- /dev/null +++ b/platform/javascript/js/libs/library_godot_eval.js @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* 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..6f80f3b958 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_http_request.js @@ -0,0 +1,143 @@ +/*************************************************************************/ +/* 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() { + var idMax = GodotHTTPRequest.requests.length; + for (var 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() { + var 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) { + var headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); + return headers === null ? 0 : GodotRuntime.strlen(headers); + }, + + godot_xhr_get_response_headers: function(xhrId, dst, len) { + var str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); + if (str === null) + return; + GodotRuntime.stringToHeap(str, dst, len); + }, + + godot_xhr_get_response_length: function(xhrId) { + var body = GodotHTTPRequest.requests[xhrId].response; + return body === null ? 0 : body.byteLength; + }, + + godot_xhr_get_response: function(xhrId, dst, len) { + var 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..582f04cb1b --- /dev/null +++ b/platform/javascript/js/libs/library_godot_os.js @@ -0,0 +1,280 @@ +/*************************************************************************/ +/* 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'] ? true : false; + 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..1769f83623 --- /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); diff --git a/platform/javascript/native/audio.worklet.js b/platform/javascript/native/audio.worklet.js deleted file mode 100644 index f91787b993..0000000000 --- a/platform/javascript/native/audio.worklet.js +++ /dev/null @@ -1,185 +0,0 @@ -/*************************************************************************/ -/* 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; - } - } - - 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 = this.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) { - this.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 = this.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); - this.write_output(output, this.output_buffer); - } else { - this.port.postMessage("Output buffer has not enough frames! Skipping output frame."); - } - } - this.process_notify(); - return true; - } - - 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]; - } - } - } - - 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/native/http_request.js b/platform/javascript/native/http_request.js deleted file mode 100644 index 272154aee3..0000000000 --- a/platform/javascript/native/http_request.js +++ /dev/null @@ -1,144 +0,0 @@ -/*************************************************************************/ -/* 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. */ -/*************************************************************************/ -var GodotHTTPRequest = { - $GodotHTTPRequest: { - requests: [], - - getUnusedRequestId: function() { - var idMax = GodotHTTPRequest.requests.length; - for (var 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() { - var 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, user, password) { - user = user > 0 ? UTF8ToString(user) : null; - password = password > 0 ? UTF8ToString(password) : null; - GodotHTTPRequest.requests[xhrId].open(UTF8ToString(method), UTF8ToString(url), true, user, password); - }, - - godot_xhr_set_request_header: function(xhrId, header, value) { - GodotHTTPRequest.requests[xhrId].setRequestHeader(UTF8ToString(header), UTF8ToString(value)); - }, - - godot_xhr_send_null: function(xhrId) { - GodotHTTPRequest.requests[xhrId].send(); - }, - - godot_xhr_send_string: function(xhrId, strPtr) { - if (!strPtr) { - err("Failed to send string per XHR: null pointer"); - return; - } - GodotHTTPRequest.requests[xhrId].send(UTF8ToString(strPtr)); - }, - - godot_xhr_send_data: function(xhrId, ptr, len) { - if (!ptr) { - err("Failed to send data per XHR: null pointer"); - return; - } - if (len < 0) { - err("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) { - var headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); - return headers === null ? 0 : lengthBytesUTF8(headers); - }, - - godot_xhr_get_response_headers: function(xhrId, dst, len) { - var str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); - if (str === null) - return; - var buf = new Uint8Array(len + 1); - stringToUTF8Array(str, buf, 0, buf.length); - buf = buf.subarray(0, -1); - HEAPU8.set(buf, dst); - }, - - godot_xhr_get_response_length: function(xhrId) { - var body = GodotHTTPRequest.requests[xhrId].response; - return body === null ? 0 : body.byteLength; - }, - - godot_xhr_get_response: function(xhrId, dst, len) { - var 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/native/library_godot_audio.js b/platform/javascript/native/library_godot_audio.js deleted file mode 100644 index 3a0c8f297a..0000000000 --- a/platform/javascript/native/library_godot_audio.js +++ /dev/null @@ -1,338 +0,0 @@ -/*************************************************************************/ -/* 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: ['$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; - onstatechange(ctx.state); // Immeditately notify state. - ctx.onstatechange = function() { - let state = 0; - switch (ctx.state) { - case 'suspended': - state = 0; - break; - case 'running': - state = 1; - break; - case 'closed': - state = 2; - break; - } - onstatechange(state); - } - // Update computed latency - GodotAudio.interval = setInterval(function() { - let latency = 0; - if (ctx.baseLatency) { - latency += GodotAudio.ctx.baseLatency; - } - if (ctx.outputLatency) { - latency += GodotAudio.ctx.outputLatency; - } - onlatencyupdate(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) { out(e) }); - } else { - if (!navigator.getUserMedia) { - navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - } - navigator.getUserMedia({ - "audio": true - }, gotMediaInput, function(e) { out(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; - console.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 = GodotOS.get_func(p_state_change); - const latencyupdate = GodotOS.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'], - $GodotAudioWorklet: { - promise: null, - worklet: null, - - create: function(channels) { - const path = Module['locateFile']('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) { - console.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 = GodotOS.heapSub(HEAPF32, p_out_buf, p_out_size); - const in_buffer = GodotOS.heapSub(HEAPF32, p_in_buf, p_in_size); - const state = GodotOS.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 = GodotOS.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 = GodotOS.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 = GodotOS.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/native/library_godot_display.js b/platform/javascript/native/library_godot_display.js deleted file mode 100644 index 11bbfbc60d..0000000000 --- a/platform/javascript/native/library_godot_display.js +++ /dev/null @@ -1,477 +0,0 @@ -/*************************************************************************/ -/* 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__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(target, event, method, capture) { - this.target = target; - this.event = event; - this.method = method; - this.capture = 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 { - console.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() { - console.log("Error reading file"); - reject(); - } - reader.readAsArrayBuffer(file); - }, function(err) { - console.log("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 { - console.error("File upload not supported"); - } - new Promise(GodotDisplayDragDrop.process).then(function() { - const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/"; - 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__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayCursor.clear(); resolve(); });', - $GodotDisplayCursor__deps: ['$GodotConfig', '$GodotOS'], - $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', '$GodotOS', '$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(UTF8ToString(p_text)); - }, - - 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(); - setValue(r_x, brect.x, 'i32'); - setValue(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 = UTF8ToString(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. - console.error("Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:", e); - }); - return 0; - }, - - godot_js_display_clipboard_get_deps: ['$GodotOS'], - godot_js_display_clipboard_get: function(callback) { - const func = GodotOS.get_func(callback); - try { - navigator.clipboard.readText().then(function (result) { - const ptr = allocate(intArrayFromString(result), ALLOC_NORMAL); - func(ptr); - _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 = UTF8ToString(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([GodotOS.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(UTF8ToString(p_string)); - }, - - godot_js_display_cursor_set_custom_shape: function(p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y) { - const shape = UTF8ToString(p_shape); - const old_shape = GodotDisplayCursor.cursors[shape]; - if (p_len > 0) { - const png = new Blob([GodotOS.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 = GodotOS.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 = GodotOS.get_func(callback); - GodotDisplayListeners.add(window, 'paste', function(evt) { - const text = evt.clipboardData.getData('text'); - const ptr = allocate(intArrayFromString(text), ALLOC_NORMAL); - func(ptr); - _free(ptr); - }, false); - }, - - godot_js_display_drop_files_cb: function(callback) { - const func = GodotOS.get_func(callback) - const dropFiles = function(files) { - const args = files || []; - if (!args.length) { - return; - } - const argc = args.length; - const argv = GodotOS.allocStringArray(args); - func(argv, argc); - GodotOS.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/native/library_godot_editor_tools.js b/platform/javascript/native/library_godot_editor_tools.js deleted file mode 100644 index 202a198adb..0000000000 --- a/platform/javascript/native/library_godot_editor_tools.js +++ /dev/null @@ -1,56 +0,0 @@ -/*************************************************************************/ -/* 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 = UTF8ToString(p_path); - const name = UTF8ToString(p_name); - const mime = UTF8ToString(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/native/library_godot_eval.js b/platform/javascript/native/library_godot_eval.js deleted file mode 100644 index 44d356a4fb..0000000000 --- a/platform/javascript/native/library_godot_eval.js +++ /dev/null @@ -1,85 +0,0 @@ -/*************************************************************************/ -/* 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: ['$GodotOS'], - 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 = UTF8ToString(p_js); - let eval_ret = null; - try { - if (p_use_global_ctx) { - // indirect eval call grants global execution context - const global_eval = eval; - eval_ret = global_eval(js_code); - } else { - eval_ret = eval(js_code); - } - } catch (e) { - err(e); - } - - switch (typeof eval_ret) { - case 'boolean': - setValue(p_union_ptr, eval_ret, 'i32'); - return 1; // BOOL - - case 'number': - setValue(p_union_ptr, eval_ret, 'double'); - return 3; // REAL - - case 'string': - let array_ptr = GodotOS.allocString(eval_ret); - setValue(p_union_ptr, array_ptr , '*'); - 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 = GodotOS.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; - } - return 0; // NIL - }, -} - -mergeInto(LibraryManager.library, GodotEval); diff --git a/platform/javascript/native/library_godot_os.js b/platform/javascript/native/library_godot_os.js deleted file mode 100644 index a1424a691a..0000000000 --- a/platform/javascript/native/library_godot_os.js +++ /dev/null @@ -1,311 +0,0 @@ -/*************************************************************************/ -/* 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: { - canvas: null, - locale: "en", - resize_on_start: false, - on_execute: null, - - init_config: function(p_opts) { - GodotConfig.resize_on_start = p_opts['resizeCanvasOnStart'] ? true : false; - 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']; - }, - }, - - godot_js_config_canvas_id_get: function(p_ptr, p_ptr_max) { - stringToUTF8('#' + GodotConfig.canvas.id, p_ptr, p_ptr_max); - }, - - godot_js_config_locale_get: function(p_ptr, p_ptr_max) { - stringToUTF8(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'], - $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; - console.log("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) { - console.log("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) { - err('Already syncing!'); - return Promise.resolve(); - } - GodotFS._syncing = true; - return new Promise(function (resolve, reject) { - FS.syncfs(false, function(error) { - if (error) { - err('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'], - $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, - - get_func: function(ptr) { - return wasmTable.get(ptr); - }, - - 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); - }); - }, - - allocString: function(p_str) { - const length = lengthBytesUTF8(p_str)+1; - const c_str = _malloc(length); - stringToUTF8(p_str, c_str, length); - return c_str; - }, - - allocStringArray: function(strings) { - const size = strings.length; - const c_ptr = _malloc(size * 4); - for (let i = 0; i < size; i++) { - HEAP32[(c_ptr >> 2) + i] = GodotOS.allocString(strings[i]); - } - return c_ptr; - }, - - freeStringArray: function(c_ptr, size) { - for (let i = 0; i < size; i++) { - _free(HEAP32[(c_ptr >> 2) + i]); - } - _free(c_ptr); - }, - - heapSub: function(heap, ptr, size) { - const bytes = heap.BYTES_PER_ELEMENT; - return heap.subarray(ptr / bytes, ptr / bytes + size); - }, - - heapCopy: function(heap, ptr, size) { - const bytes = heap.BYTES_PER_ELEMENT; - return heap.slice(ptr / bytes, ptr / bytes + size); - }, - }, - - godot_js_os_finish_async: function(p_callback) { - const func = GodotOS.get_func(p_callback); - GodotOS.finish_async(func); - }, - - godot_js_os_request_quit_cb: function(p_callback) { - GodotOS.request_quit = GodotOS.get_func(p_callback); - }, - - godot_js_os_fs_is_persistent: function() { - return GodotFS.is_persistent(); - }, - - godot_js_os_fs_sync: function(callback) { - const func = GodotOS.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 = UTF8ToString(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(UTF8ToString(p_uri), '_blank'); - }, -}; - -autoAddDeps(GodotOS, '$GodotOS'); -mergeInto(LibraryManager.library, GodotOS); -- cgit v1.2.3