diff options
Diffstat (limited to 'platform/javascript')
48 files changed, 6475 insertions, 2659 deletions
diff --git a/platform/javascript/.eslintrc.engine.js b/platform/javascript/.eslintrc.engine.js new file mode 100644 index 0000000000..00f0f147a9 --- /dev/null +++ b/platform/javascript/.eslintrc.engine.js @@ -0,0 +1,10 @@ +module.exports = { + "extends": [ + "./.eslintrc.js", + ], + "globals": { + "Godot": true, + "Preloader": true, + "Utils": true, + }, +}; diff --git a/platform/javascript/.eslintrc.js b/platform/javascript/.eslintrc.js new file mode 100644 index 0000000000..0ff9d67d26 --- /dev/null +++ b/platform/javascript/.eslintrc.js @@ -0,0 +1,43 @@ +module.exports = { + "env": { + "browser": true, + "es2021": true, + }, + "extends": [ + "airbnb-base", + ], + "parserOptions": { + "ecmaVersion": 12, + }, + "ignorePatterns": "*.externs.js", + "rules": { + "func-names": "off", + // Use tabs for consistency with the C++ codebase. + "indent": ["error", "tab"], + "max-len": "off", + "no-else-return": ["error", {allowElseIf: true}], + "curly": ["error", "all"], + "brace-style": ["error", "1tbs", { "allowSingleLine": false }], + "no-bitwise": "off", + "no-continue": "off", + "no-self-assign": "off", + "no-tabs": "off", + "no-param-reassign": ["error", { "props": false }], + "no-plusplus": "off", + "no-unused-vars": ["error", { "args": "none" }], + "prefer-destructuring": "off", + "prefer-rest-params": "off", + "prefer-spread": "off", + "camelcase": "off", + "no-underscore-dangle": "off", + "max-classes-per-file": "off", + "prefer-arrow-callback": "off", + // Messes up with copyright headers in source files. + "spaced-comment": "off", + // Completely breaks emscripten libraries. + "object-shorthand": "off", + // Closure compiler (exported properties) + "quote-props": ["error", "consistent"], + "dot-notation": "off", + } +}; diff --git a/platform/javascript/.eslintrc.libs.js b/platform/javascript/.eslintrc.libs.js new file mode 100644 index 0000000000..81b1b8c864 --- /dev/null +++ b/platform/javascript/.eslintrc.libs.js @@ -0,0 +1,25 @@ +module.exports = { + "extends": [ + "./.eslintrc.js", + ], + "globals": { + "LibraryManager": true, + "mergeInto": true, + "autoAddDeps": true, + "HEAP8": true, + "HEAPU8": true, + "HEAP32": true, + "HEAPF32": true, + "ERRNO_CODES": true, + "FS": true, + "IDBFS": true, + "GodotOS": true, + "GodotConfig": true, + "GodotRuntime": true, + "GodotFS": true, + "IDHandler": true, + "Browser": true, + "GL": true, + "XRWebGLLayer": true, + }, +}; diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 7239648937..b0302a5f88 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -4,39 +4,82 @@ Import("env") javascript_files = [ "audio_driver_javascript.cpp", + "display_server_javascript.cpp", "http_client_javascript.cpp", "javascript_eval.cpp", "javascript_main.cpp", "os_javascript.cpp", + "api/javascript_tools_editor_plugin.cpp", ] -build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"] -if env["threads_enabled"]: - build_targets.append("#bin/godot${PROGSUFFIX}.worker.js") +sys_env = env.Clone() +sys_env.AddJSLibraries( + [ + "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", + ] +) -build = env.add_program(build_targets, javascript_files) +if env["tools"]: + sys_env.AddJSLibraries(["js/libs/library_godot_editor_tools.js"]) +if env["javascript_eval"]: + sys_env.AddJSLibraries(["js/libs/library_godot_eval.js"]) -js_libraries = [ - "http_request.js", -] -for lib in js_libraries: - env.Append(LINKFLAGS=["--js-library", env.File(lib).path]) -env.Depends(build, js_libraries) +for lib in sys_env["JS_LIBS"]: + sys_env.Append(LINKFLAGS=["--js-library", lib]) +for js in env["JS_PRE"]: + sys_env.Append(LINKFLAGS=["--pre-js", env.File(js).path]) +for ext in env["JS_EXTERNS"]: + sys_env["ENV"]["EMCC_CLOSURE_ARGS"] += " --externs " + ext.path -js_modules = [ - "id_handler.js", -] -for module in js_modules: - env.Append(LINKFLAGS=["--pre-js", env.File(module).path]) -env.Depends(build, js_modules) +build = [] +if env["gdnative_enabled"]: + build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"] + # Reset libraries. The main runtime will only link emscripten libraries, not godot ones. + sys_env["LIBS"] = [] + # We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly. + sys_env.Append(LIBS=["idbfs.js"]) + # JS prepended to the module code loading the side library. + sys_env.Append(LINKFLAGS=["--pre-js", sys_env.File("js/dynlink.pre.js")]) + # Configure it as a main module (dynamic linking support). + sys_env.Append(CCFLAGS=["-s", "MAIN_MODULE=1"]) + sys_env.Append(LINKFLAGS=["-s", "MAIN_MODULE=1"]) + sys_env.Append(CCFLAGS=["-s", "EXPORT_ALL=1"]) + sys_env.Append(LINKFLAGS=["-s", "EXPORT_ALL=1"]) + # Force exporting the standard library (printf, malloc, etc.) + sys_env["ENV"]["EMCC_FORCE_STDLIBS"] = "libc,libc++,libc++abi" + # The main emscripten runtime, with exported standard libraries. + sys = sys_env.Program(build_targets, ["javascript_runtime.cpp"]) + sys_env.Depends(sys, "js/dynlink.pre.js") + + # The side library, containing all Godot code. + wasm_env = env.Clone() + wasm_env.Append(CPPDEFINES=["WASM_GDNATIVE"]) # So that OS knows it can run GDNative libraries. + wasm_env.Append(CCFLAGS=["-s", "SIDE_MODULE=2"]) + wasm_env.Append(LINKFLAGS=["-s", "SIDE_MODULE=2"]) + wasm = wasm_env.add_program("#bin/godot.side${PROGSUFFIX}.wasm", javascript_files) + build = [sys[0], sys[1], wasm[0]] +else: + build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"] + if env["threads_enabled"]: + build_targets.append("#bin/godot${PROGSUFFIX}.worker.js") + # We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly. + sys_env.Append(LIBS=["idbfs.js"]) + build = sys_env.Program(build_targets, javascript_files + ["javascript_runtime.cpp"]) + +sys_env.Depends(build[0], sys_env["JS_LIBS"]) +sys_env.Depends(build[0], sys_env["JS_PRE"]) +sys_env.Depends(build[0], sys_env["JS_EXTERNS"]) engine = [ - "engine/preloader.js", - "engine/loader.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) @@ -47,11 +90,27 @@ wrap_list = [ js_wrapped = env.Textfile("#bin/godot", [env.File(f) for f in wrap_list], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js") zip_dir = env.Dir("#bin/.javascript_zip") -out_files = [zip_dir.File("godot.js"), zip_dir.File("godot.wasm"), zip_dir.File("godot.html")] -in_files = [js_wrapped, build[1], "#misc/dist/html/full-size.html"] -if env["threads_enabled"]: - in_files.append(build[2]) - out_files.append(zip_dir.File("godot.worker.js")) +binary_name = "godot.tools" if env["tools"] else "godot" +out_files = [ + zip_dir.File(binary_name + ".js"), + zip_dir.File(binary_name + ".wasm"), + zip_dir.File(binary_name + ".html"), + 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/js/libs/audio.worklet.js"] +if env["gdnative_enabled"]: + in_files.append(build[2]) # Runtime + out_files.append(zip_dir.File(binary_name + ".side.wasm")) +elif env["threads_enabled"]: + in_files.append(build[2]) # Worker + out_files.append(zip_dir.File(binary_name + ".worker.js")) + +if env["tools"]: + in_files.append("#misc/dist/html/logo.svg") + out_files.append(zip_dir.File("logo.svg")) + in_files.append("#icon.png") + out_files.append(zip_dir.File("favicon.png")) zip_files = env.InstallAs(out_files, in_files) env.Zip( @@ -59,5 +118,5 @@ env.Zip( zip_files, ZIPROOT=zip_dir, ZIPSUFFIX="${PROGSUFFIX}${ZIPSUFFIX}", - ZIPCOMSTR="Archving $SOURCES as $TARGET", + ZIPCOMSTR="Archiving $SOURCES as $TARGET", ) diff --git a/platform/javascript/api/api.cpp b/platform/javascript/api/api.cpp index 45cb82b351..2f7bde065f 100644 --- a/platform/javascript/api/api.cpp +++ b/platform/javascript/api/api.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -29,32 +29,30 @@ /*************************************************************************/ #include "api.h" -#include "core/engine.h" +#include "core/config/engine.h" #include "javascript_eval.h" +#include "javascript_tools_editor_plugin.h" static JavaScript *javascript_eval; void register_javascript_api() { - + JavaScriptToolsEditorPlugin::initialize(); ClassDB::register_virtual_class<JavaScript>(); javascript_eval = memnew(JavaScript); Engine::get_singleton()->add_singleton(Engine::Singleton("JavaScript", javascript_eval)); } void unregister_javascript_api() { - memdelete(javascript_eval); } JavaScript *JavaScript::singleton = nullptr; JavaScript *JavaScript::get_singleton() { - return singleton; } JavaScript::JavaScript() { - ERR_FAIL_COND_MSG(singleton != nullptr, "JavaScript singleton already exist."); singleton = this; } @@ -62,13 +60,11 @@ JavaScript::JavaScript() { JavaScript::~JavaScript() {} void JavaScript::_bind_methods() { - ClassDB::bind_method(D_METHOD("eval", "code", "use_global_execution_context"), &JavaScript::eval, DEFVAL(false)); } #if !defined(JAVASCRIPT_ENABLED) || !defined(JAVASCRIPT_EVAL_ENABLED) Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { - return Variant(); } #endif diff --git a/platform/javascript/api/api.h b/platform/javascript/api/api.h index 8afe0f33ce..2ac7333cdd 100644 --- a/platform/javascript/api/api.h +++ b/platform/javascript/api/api.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/platform/javascript/api/javascript_eval.h b/platform/javascript/api/javascript_eval.h index 29229de8e3..24f7648ed9 100644 --- a/platform/javascript/api/javascript_eval.h +++ b/platform/javascript/api/javascript_eval.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -31,7 +31,7 @@ #ifndef JAVASCRIPT_EVAL_H #define JAVASCRIPT_EVAL_H -#include "core/object.h" +#include "core/object/class_db.h" class JavaScript : public Object { private: diff --git a/platform/javascript/api/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp new file mode 100644 index 0000000000..8355faccc2 --- /dev/null +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -0,0 +1,135 @@ +/*************************************************************************/ +/* javascript_tools_editor_plugin.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#if defined(TOOLS_ENABLED) && defined(JAVASCRIPT_ENABLED) +#include "javascript_tools_editor_plugin.h" + +#include "core/config/engine.h" +#include "core/config/project_settings.h" +#include "core/os/dir_access.h" +#include "core/os/file_access.h" +#include "editor/editor_node.h" + +#include <emscripten/emscripten.h> + +// JavaScript functions defined in library_godot_editor_tools.js +extern "C" { +extern void godot_js_editor_download_file(const char *p_path, const char *p_name, const char *p_mime); +} + +static void _javascript_editor_init_callback() { + EditorNode::get_singleton()->add_editor_plugin(memnew(JavaScriptToolsEditorPlugin(EditorNode::get_singleton()))); +} + +void JavaScriptToolsEditorPlugin::initialize() { + EditorNode::add_init_callback(_javascript_editor_init_callback); +} + +JavaScriptToolsEditorPlugin::JavaScriptToolsEditorPlugin(EditorNode *p_editor) { + add_tool_menu_item("Download Project Source", callable_mp(this, &JavaScriptToolsEditorPlugin::_download_zip)); +} + +void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { + if (!Engine::get_singleton() || !Engine::get_singleton()->is_editor_hint()) { + WARN_PRINT("Project download is only available in Editor mode"); + return; + } + String resource_path = ProjectSettings::get_singleton()->get_resource_path(); + + FileAccess *src_f; + zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zipFile zip = zipOpen2("/tmp/project.zip", APPEND_STATUS_CREATE, NULL, &io); + String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; + _zip_recursive(resource_path, base_path, zip); + zipClose(zip, NULL); + godot_js_editor_download_file("/tmp/project.zip", "project.zip", "application/zip"); +} + +void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, zipFile p_zip) { + FileAccess *f = FileAccess::open(p_path, FileAccess::READ); + if (!f) { + WARN_PRINT("Unable to open file for zipping: " + p_path); + return; + } + Vector<uint8_t> data; + int len = f->get_len(); + data.resize(len); + f->get_buffer(data.ptrw(), len); + f->close(); + memdelete(f); + + String path = p_path.replace_first(p_base_path, ""); + zipOpenNewFileInZip(p_zip, + path.utf8().get_data(), + NULL, + NULL, + 0, + NULL, + 0, + NULL, + Z_DEFLATED, + Z_DEFAULT_COMPRESSION); + zipWriteInFileInZip(p_zip, data.ptr(), data.size()); + zipCloseFileInZip(p_zip); +} + +void JavaScriptToolsEditorPlugin::_zip_recursive(String p_path, String p_base_path, zipFile p_zip) { + DirAccess *dir = DirAccess::open(p_path); + if (!dir) { + WARN_PRINT("Unable to open dir for zipping: " + p_path); + return; + } + dir->list_dir_begin(); + String cur = dir->get_next(); + while (!cur.is_empty()) { + String cs = p_path.plus_file(cur); + if (cur == "." || cur == ".." || cur == ".import") { + // Skip + } else if (dir->current_is_dir()) { + String path = cs.replace_first(p_base_path, "") + "/"; + zipOpenNewFileInZip(p_zip, + path.utf8().get_data(), + NULL, + NULL, + 0, + NULL, + 0, + NULL, + Z_DEFLATED, + Z_DEFAULT_COMPRESSION); + zipCloseFileInZip(p_zip); + _zip_recursive(cs, p_base_path, p_zip); + } else { + _zip_file(cs, p_base_path, p_zip); + } + cur = dir->get_next(); + } +} +#endif diff --git a/platform/javascript/api/javascript_tools_editor_plugin.h b/platform/javascript/api/javascript_tools_editor_plugin.h new file mode 100644 index 0000000000..557821d627 --- /dev/null +++ b/platform/javascript/api/javascript_tools_editor_plugin.h @@ -0,0 +1,58 @@ +/*************************************************************************/ +/* javascript_tools_editor_plugin.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef JAVASCRIPT_TOOLS_EDITOR_PLUGIN_H +#define JAVASCRIPT_TOOLS_EDITOR_PLUGIN_H + +#if defined(TOOLS_ENABLED) && defined(JAVASCRIPT_ENABLED) +#include "core/io/zip_io.h" +#include "editor/editor_plugin.h" + +class JavaScriptToolsEditorPlugin : public EditorPlugin { + GDCLASS(JavaScriptToolsEditorPlugin, EditorPlugin); + +private: + void _zip_file(String p_path, String p_base_path, zipFile p_zip); + void _zip_recursive(String p_path, String p_base_path, zipFile p_zip); + void _download_zip(Variant p_v); + +public: + static void initialize(); + + JavaScriptToolsEditorPlugin(EditorNode *p_editor); +}; +#else +class JavaScriptToolsEditorPlugin { +public: + static void initialize() {} +}; +#endif + +#endif // JAVASCRIPT_TOOLS_EDITOR_PLUGIN_H diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 8f857478e5..6395fdf721 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,242 +30,258 @@ #include "audio_driver_javascript.h" +#include "core/config/project_settings.h" + #include <emscripten.h> AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr; -const char *AudioDriverJavaScript::get_name() const { +bool AudioDriverJavaScript::is_available() { + return godot_audio_is_available() != 0; +} +const char *AudioDriverJavaScript::get_name() const { return "JavaScript"; } -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_js_mix() { - - AudioDriverJavaScript::singleton->mix_to_js(); +void AudioDriverJavaScript::_state_change_callback(int p_state) { + singleton->state = p_state; } -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) { - - AudioDriverJavaScript::singleton->process_capture(sample); +void AudioDriverJavaScript::_latency_update_callback(float p_latency) { + singleton->output_latency = p_latency; } -void AudioDriverJavaScript::mix_to_js() { +void AudioDriverJavaScript::_audio_driver_process(int p_from, int p_samples) { + int32_t *stream_buffer = reinterpret_cast<int32_t *>(output_rb); + const int max_samples = memarr_len(output_rb); - int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); - int sample_count = memarr_len(internal_buffer) / channel_count; - int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer); - audio_server_process(sample_count, stream_buffer); - for (int i = 0; i < sample_count * channel_count; i++) { - internal_buffer[i] = float(stream_buffer[i] >> 16) / 32768.f; + int write_pos = p_from; + int to_write = p_samples; + if (to_write == 0) { + to_write = max_samples; + } + // High part + if (write_pos + to_write > max_samples) { + const int samples_high = max_samples - write_pos; + audio_server_process(samples_high / channel_count, &stream_buffer[write_pos]); + for (int i = write_pos; i < max_samples; i++) { + output_rb[i] = float(stream_buffer[i] >> 16) / 32768.f; + } + to_write -= samples_high; + write_pos = 0; + } + // Leftover + audio_server_process(to_write / channel_count, &stream_buffer[write_pos]); + for (int i = write_pos; i < write_pos + to_write; i++) { + output_rb[i] = float(stream_buffer[i] >> 16) / 32768.f; } } -void AudioDriverJavaScript::process_capture(float sample) { +void AudioDriverJavaScript::_audio_driver_capture(int p_from, int p_samples) { + if (get_input_buffer().size() == 0) { + return; // Input capture stopped. + } + const int max_samples = memarr_len(input_rb); - int32_t sample32 = int32_t(sample * 32768.f) * (1U << 16); - input_buffer_write(sample32); + int read_pos = p_from; + int to_read = p_samples; + if (to_read == 0) { + to_read = max_samples; + } + // High part + if (read_pos + to_read > max_samples) { + const int samples_high = max_samples - read_pos; + for (int i = read_pos; i < max_samples; i++) { + input_buffer_write(int32_t(input_rb[i] * 32768.f) * (1U << 16)); + } + to_read -= samples_high; + read_pos = 0; + } + // Leftover + for (int i = read_pos; i < read_pos + to_read; i++) { + input_buffer_write(int32_t(input_rb[i] * 32768.f) * (1U << 16)); + } } Error AudioDriverJavaScript::init() { - - /* clang-format off */ - _driver_id = EM_ASM_INT({ - return Module.IDHandler.add({ - 'context': new (window.AudioContext || window.webkitAudioContext), - 'input': null, - 'stream': null, - 'script': null - }); - }); - /* clang-format on */ - - int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); - /* clang-format off */ - buffer_length = EM_ASM_INT({ - var ref = Module.IDHandler.get($0); - var ctx = ref['context']; - var CHANNEL_COUNT = $1; - - var channelCount = ctx.destination.channelCount; - var script = null; - try { - // Try letting the browser recommend a buffer length. - script = ctx.createScriptProcessor(0, 2, channelCount); - } catch (e) { - // ...otherwise, default to 4096. - script = ctx.createScriptProcessor(4096, 2, channelCount); - } - script.connect(ctx.destination); - ref['script'] = script; - - return script.bufferSize; - }, _driver_id, channel_count); - /* clang-format on */ - if (!buffer_length) { - return FAILED; + mix_rate = GLOBAL_GET("audio/mix_rate"); + int latency = GLOBAL_GET("audio/output_latency"); + + channel_count = godot_audio_init(mix_rate, latency, &_state_change_callback, &_latency_update_callback); + buffer_length = closest_power_of_2((latency * mix_rate / 1000)); +#ifndef NO_THREADS + node = memnew(WorkletNode); +#else + node = memnew(ScriptProcessorNode); +#endif + buffer_length = node->create(buffer_length, channel_count); + if (output_rb) { + memdelete_arr(output_rb); } - - if (!internal_buffer || (int)memarr_len(internal_buffer) != buffer_length * channel_count) { - if (internal_buffer) - memdelete_arr(internal_buffer); - internal_buffer = memnew_arr(float, buffer_length *channel_count); + output_rb = memnew_arr(float, buffer_length *channel_count); + if (!output_rb) { + return ERR_OUT_OF_MEMORY; } - - return internal_buffer ? OK : ERR_OUT_OF_MEMORY; + if (input_rb) { + memdelete_arr(input_rb); + } + input_rb = memnew_arr(float, buffer_length *channel_count); + if (!input_rb) { + return ERR_OUT_OF_MEMORY; + } + return OK; } void AudioDriverJavaScript::start() { - - /* clang-format off */ - EM_ASM({ - const ref = Module.IDHandler.get($0); - var INTERNAL_BUFFER_PTR = $1; - - var audioDriverMixFunction = cwrap('audio_driver_js_mix'); - var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']); - ref['script'].onaudioprocess = function(audioProcessingEvent) { - audioDriverMixFunction(); - - var input = audioProcessingEvent.inputBuffer; - var output = audioProcessingEvent.outputBuffer; - var internalBuffer = HEAPF32.subarray( - INTERNAL_BUFFER_PTR / HEAPF32.BYTES_PER_ELEMENT, - INTERNAL_BUFFER_PTR / HEAPF32.BYTES_PER_ELEMENT + output.length * output.numberOfChannels); - - for (var channel = 0; channel < output.numberOfChannels; channel++) { - var outputData = output.getChannelData(channel); - // Loop through samples. - for (var sample = 0; sample < outputData.length; sample++) { - outputData[sample] = internalBuffer[sample * output.numberOfChannels + channel]; - } - } - - if (ref['input']) { - var inputDataL = input.getChannelData(0); - var inputDataR = input.getChannelData(1); - for (var i = 0; i < inputDataL.length; i++) { - audioDriverProcessCapture(inputDataL[i]); - audioDriverProcessCapture(inputDataR[i]); - } - } - }; - }, _driver_id, internal_buffer); - /* clang-format on */ + if (node) { + node->start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb)); + } } void AudioDriverJavaScript::resume() { - /* clang-format off */ - EM_ASM({ - const ref = Module.IDHandler.get($0); - if (ref && ref['context'] && ref['context'].resume) - ref['context'].resume(); - }, _driver_id); - /* clang-format on */ + if (state == 0) { // 'suspended' + godot_audio_resume(); + } } -int AudioDriverJavaScript::get_mix_rate() const { +float AudioDriverJavaScript::get_latency() { + return output_latency + (float(buffer_length) / mix_rate); +} - /* clang-format off */ - return EM_ASM_INT({ - const ref = Module.IDHandler.get($0); - return ref && ref['context'] ? ref['context'].sampleRate : 0; - }, _driver_id); - /* clang-format on */ +int AudioDriverJavaScript::get_mix_rate() const { + return mix_rate; } AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const { - - /* clang-format off */ - return get_speaker_mode_by_total_channels(EM_ASM_INT({ - const ref = Module.IDHandler.get($0); - return ref && ref['context'] ? ref['context'].destination.channelCount : 0; - }, _driver_id)); - /* clang-format on */ + return get_speaker_mode_by_total_channels(channel_count); } -// No locking, as threads are not supported. void AudioDriverJavaScript::lock() { + if (node) { + node->unlock(); + } } void AudioDriverJavaScript::unlock() { + if (node) { + node->unlock(); + } } void AudioDriverJavaScript::finish() { - - /* clang-format off */ - EM_ASM({ - Module.IDHandler.remove($0); - }, _driver_id); - /* clang-format on */ - - if (internal_buffer) { - memdelete_arr(internal_buffer); - internal_buffer = nullptr; + if (node) { + node->finish(); + memdelete(node); + node = nullptr; + } + if (output_rb) { + memdelete_arr(output_rb); + output_rb = nullptr; + } + if (input_rb) { + memdelete_arr(input_rb); + input_rb = nullptr; } - _driver_id = 0; } Error AudioDriverJavaScript::capture_start() { - + lock(); input_buffer_init(buffer_length); + unlock(); + if (godot_audio_capture_start()) { + return FAILED; + } + return OK; +} - /* clang-format off */ - EM_ASM({ - function gotMediaInput(stream) { - var ref = Module.IDHandler.get($0); - ref['stream'] = stream; - ref['input'] = ref['context'].createMediaStreamSource(stream); - ref['input'].connect(ref['script']); - } - - function gotMediaInputError(e) { - out(e); - } +Error AudioDriverJavaScript::capture_stop() { + godot_audio_capture_stop(); + lock(); + input_buffer.clear(); + unlock(); + return OK; +} - if (navigator.mediaDevices.getUserMedia) { - navigator.mediaDevices.getUserMedia({"audio": true}).then(gotMediaInput, gotMediaInputError); - } else { - if (!navigator.getUserMedia) - navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError); - } - }, _driver_id); - /* clang-format on */ +AudioDriverJavaScript::AudioDriverJavaScript() { + singleton = this; +} - return OK; +#ifdef NO_THREADS +/// ScriptProcessorNode implementation +void AudioDriverJavaScript::ScriptProcessorNode::_process_callback() { + AudioDriverJavaScript::singleton->_audio_driver_capture(); + AudioDriverJavaScript::singleton->_audio_driver_process(); } -Error AudioDriverJavaScript::capture_stop() { +int AudioDriverJavaScript::ScriptProcessorNode::create(int p_buffer_samples, int p_channels) { + return godot_audio_script_create(p_buffer_samples, p_channels); +} - /* clang-format off */ - EM_ASM({ - var ref = Module.IDHandler.get($0); - if (ref['stream']) { - const tracks = ref['stream'].getTracks(); - for (var i = 0; i < tracks.length; i++) { - tracks[i].stop(); +void AudioDriverJavaScript::ScriptProcessorNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { + godot_audio_script_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, &_process_callback); +} +#else +/// AudioWorkletNode implementation +void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) { + AudioDriverJavaScript::WorkletNode *obj = static_cast<AudioDriverJavaScript::WorkletNode *>(p_data); + AudioDriverJavaScript *driver = AudioDriverJavaScript::singleton; + const int out_samples = memarr_len(driver->output_rb); + const int in_samples = memarr_len(driver->input_rb); + int wpos = 0; + int to_write = out_samples; + int rpos = 0; + int to_read = 0; + int32_t step = 0; + while (!obj->quit) { + if (to_read) { + driver->lock(); + driver->_audio_driver_capture(rpos, to_read); + godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_IN, -to_read); + driver->unlock(); + rpos += to_read; + if (rpos >= in_samples) { + rpos -= in_samples; } - ref['stream'] = null; } - - if (ref['input']) { - ref['input'].disconnect(); - ref['input'] = null; + if (to_write) { + driver->lock(); + driver->_audio_driver_process(wpos, to_write); + godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_OUT, to_write); + driver->unlock(); + wpos += to_write; + if (wpos >= out_samples) { + wpos -= out_samples; + } } + step = godot_audio_worklet_state_wait(obj->state, STATE_PROCESS, step, 1); + to_write = out_samples - godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_OUT); + to_read = godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_IN); + } +} - }, _driver_id); - /* clang-format on */ - - input_buffer.clear(); +int AudioDriverJavaScript::WorkletNode::create(int p_buffer_size, int p_channels) { + godot_audio_worklet_create(p_channels); + return p_buffer_size; +} - return OK; +void AudioDriverJavaScript::WorkletNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { + godot_audio_worklet_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, state); + thread = Thread::create(_audio_thread_func, this); } -AudioDriverJavaScript::AudioDriverJavaScript() { +void AudioDriverJavaScript::WorkletNode::lock() { + mutex.lock(); +} - _driver_id = 0; - internal_buffer = nullptr; - buffer_length = 0; +void AudioDriverJavaScript::WorkletNode::unlock() { + mutex.unlock(); +} - singleton = this; +void AudioDriverJavaScript::WorkletNode::finish() { + quit = true; // Ask thread to quit. + Thread::wait_to_finish(thread); + memdelete(thread); + thread = nullptr; } +#endif diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index f6f2dacd4e..d55ec261a4 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -31,18 +31,78 @@ #ifndef AUDIO_DRIVER_JAVASCRIPT_H #define AUDIO_DRIVER_JAVASCRIPT_H +#include "core/os/mutex.h" +#include "core/os/thread.h" #include "servers/audio_server.h" +#include "godot_audio.h" + class AudioDriverJavaScript : public AudioDriver { +public: + class AudioNode { + public: + virtual int create(int p_buffer_size, int p_output_channels) = 0; + virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0; + virtual void finish() {} + virtual void lock() {} + virtual void unlock() {} + virtual ~AudioNode() {} + }; + + class WorkletNode : public AudioNode { + private: + enum { + STATE_LOCK, + STATE_PROCESS, + STATE_SAMPLES_IN, + STATE_SAMPLES_OUT, + STATE_MAX, + }; + Mutex mutex; + Thread *thread = nullptr; + bool quit = false; + int32_t state[STATE_MAX] = { 0 }; + + static void _audio_thread_func(void *p_data); + + public: + int create(int p_buffer_size, int p_output_channels) override; + void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; + void finish() override; + void lock() override; + void unlock() override; + }; + + class ScriptProcessorNode : public AudioNode { + private: + static void _process_callback(); - float *internal_buffer; + public: + int create(int p_buffer_samples, int p_channels) override; + void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; + }; - int _driver_id; - int buffer_length; +private: + AudioNode *node = nullptr; + + float *output_rb = nullptr; + float *input_rb = nullptr; + + int buffer_length = 0; + int mix_rate = 0; + int channel_count = 0; + int state = 0; + float output_latency = 0.0; + + static void _state_change_callback(int p_state); + static void _latency_update_callback(float p_latency); + +protected: + void _audio_driver_process(int p_from = 0, int p_samples = 0); + void _audio_driver_capture(int p_from = 0, int p_samples = 0); public: - void mix_to_js(); - void process_capture(float sample); + static bool is_available(); static AudioDriverJavaScript *singleton; @@ -51,6 +111,7 @@ public: virtual Error init(); virtual void start(); void resume(); + virtual float get_latency(); virtual int get_mix_rate() const; virtual SpeakerMode get_speaker_mode() const; virtual void lock(); @@ -62,5 +123,4 @@ public: AudioDriverJavaScript(); }; - #endif diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 9486e10717..0d57f8aad1 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -1,6 +1,9 @@ import os +import sys -from emscripten_helpers import parse_config, run_closure_compiler, create_engine_file +from emscripten_helpers import run_closure_compiler, create_engine_file, add_js_libraries, add_js_pre, add_js_externs +from methods import get_compiler_version +from SCons.Util import WhereIs def is_active(): @@ -12,17 +15,25 @@ def get_name(): def can_build(): - return "EM_CONFIG" in os.environ or os.path.exists(os.path.expanduser("~/.emscripten")) + return WhereIs("emcc") is not None def get_opts(): from SCons.Variables import BoolVariable return [ + ("initial_memory", "Initial WASM memory (in MiB)", 16), + BoolVariable("use_assertions", "Use Emscripten runtime assertions", False), + BoolVariable("use_thinlto", "Use ThinLTO", False), + BoolVariable("use_ubsan", "Use Emscripten undefined behavior sanitizer (UBSAN)", False), + BoolVariable("use_asan", "Use Emscripten address sanitizer (ASAN)", False), + BoolVariable("use_lsan", "Use Emscripten leak sanitizer (LSAN)", False), + BoolVariable("use_safe_heap", "Use Emscripten SAFE_HEAP sanitizer", False), # eval() can be a security concern, so it can be disabled. BoolVariable("javascript_eval", "Enable JavaScript eval interface", True), BoolVariable("threads_enabled", "Enable WebAssembly Threads support (limited browser support)", False), - BoolVariable("use_closure_compiler", "Use closure compiler to minimize Javascript code", False), + BoolVariable("gdnative_enabled", "Enable WebAssembly GDNative support (produces bigger binaries)", False), + BoolVariable("use_closure_compiler", "Use closure compiler to minimize JavaScript code", False), ] @@ -39,6 +50,9 @@ def get_flags(): def configure(env): + if not isinstance(env["initial_memory"], int): + print("Initial memory must be a valid integer") + sys.exit(255) ## Build type @@ -57,19 +71,24 @@ def configure(env): env.Append(CPPDEFINES=["DEBUG_ENABLED"]) # Retain function names for backtraces at the cost of file size. env.Append(LINKFLAGS=["--profiling-funcs"]) - else: # 'debug' + else: # "debug" env.Append(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(CCFLAGS=["-O1", "-g"]) env.Append(LINKFLAGS=["-O1", "-g"]) + env["use_assertions"] = True + + if env["use_assertions"]: env.Append(LINKFLAGS=["-s", "ASSERTIONS=1"]) if env["tools"]: if not env["threads_enabled"]: - raise RuntimeError( - "Threads must be enabled to build the editor. Please add the 'threads_enabled=yes' option" - ) - # Tools need more memory. Initial stack memory in bytes. See `src/settings.js` in emscripten repository (will be renamed to INITIAL_MEMORY). - env.Append(LINKFLAGS=["-s", "TOTAL_MEMORY=33554432"]) + print("Threads must be enabled to build the editor. Please add the 'threads_enabled=yes' option") + sys.exit(255) + if env["initial_memory"] < 32: + print("Editor build requires at least 32MiB of initial memory. Forcing it.") + env["initial_memory"] = 32 + elif env["builtin_icu"]: + env.Append(CCFLAGS=["-frtti"]) else: # Disable exceptions and rtti on non-tools (template) builds # These flags help keep the file size down. @@ -77,14 +96,32 @@ def configure(env): # Don't use dynamic_cast, necessary with no-rtti. env.Append(CPPDEFINES=["NO_SAFE_CAST"]) + env.Append(LINKFLAGS=["-s", "INITIAL_MEMORY=%sMB" % env["initial_memory"]]) + ## Copy env variables. env["ENV"] = os.environ # LTO - if env["use_lto"]: - env.Append(CCFLAGS=["-s", "WASM_OBJECT_FILES=0"]) - env.Append(LINKFLAGS=["-s", "WASM_OBJECT_FILES=0"]) - env.Append(LINKFLAGS=["--llvm-lto", "1"]) + if env["use_thinlto"]: + env.Append(CCFLAGS=["-flto=thin"]) + env.Append(LINKFLAGS=["-flto=thin"]) + elif env["use_lto"]: + env.Append(CCFLAGS=["-flto=full"]) + env.Append(LINKFLAGS=["-flto=full"]) + + # Sanitizers + if env["use_ubsan"]: + env.Append(CCFLAGS=["-fsanitize=undefined"]) + env.Append(LINKFLAGS=["-fsanitize=undefined"]) + if env["use_asan"]: + env.Append(CCFLAGS=["-fsanitize=address"]) + env.Append(LINKFLAGS=["-fsanitize=address"]) + if env["use_lsan"]: + env.Append(CCFLAGS=["-fsanitize=leak"]) + env.Append(LINKFLAGS=["-fsanitize=leak"]) + if env["use_safe_heap"]: + env.Append(CCFLAGS=["-s", "SAFE_HEAP=1"]) + env.Append(LINKFLAGS=["-s", "SAFE_HEAP=1"]) # Closure compiler if env["use_closure_compiler"]: @@ -94,18 +131,22 @@ def configure(env): jscc = env.Builder(generator=run_closure_compiler, suffix=".cc.js", src_suffix=".js") env.Append(BUILDERS={"BuildJS": jscc}) + # Add helper method for adding libraries, externs, pre-js. + env["JS_LIBS"] = [] + env["JS_PRE"] = [] + env["JS_EXTERNS"] = [] + env.AddMethod(add_js_libraries, "AddJSLibraries") + env.AddMethod(add_js_pre, "AddJSPre") + env.AddMethod(add_js_externs, "AddJSExterns") + # Add method that joins/compiles our Engine files. env.AddMethod(create_engine_file, "CreateEngineFile") # Closure compiler extern and support for ecmascript specs (const, let, etc). env["ENV"]["EMCC_CLOSURE_ARGS"] = "--language_in ECMASCRIPT6" - em_config = parse_config() - env.PrependENVPath("PATH", em_config["EMCC_ROOT"]) - env["CC"] = "emcc" env["CXX"] = "em++" - env["LINK"] = "emcc" env["AR"] = "emar" env["RANLIB"] = "emranlib" @@ -132,25 +173,35 @@ def configure(env): if env["javascript_eval"]: env.Append(CPPDEFINES=["JAVASCRIPT_EVAL_ENABLED"]) + if env["threads_enabled"] and env["gdnative_enabled"]: + print("Threads and GDNative support can't be both enabled due to WebAssembly limitations") + sys.exit(255) + # Thread support (via SharedArrayBuffer). if env["threads_enabled"]: env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"]) env.Append(CCFLAGS=["-s", "USE_PTHREADS=1"]) env.Append(LINKFLAGS=["-s", "USE_PTHREADS=1"]) - env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=4"]) + env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=8"]) env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"]) + env.extra_suffix = ".threads" + env.extra_suffix else: env.Append(CPPDEFINES=["NO_THREADS"]) + if env["gdnative_enabled"]: + major, minor, patch = get_compiler_version(env) + if major < 2 or (major == 2 and minor == 0 and patch < 10): + print("GDNative support requires emscripten >= 2.0.10, detected: %s.%s.%s" % (major, minor, patch)) + sys.exit(255) + env.Append(CCFLAGS=["-s", "RELOCATABLE=1"]) + env.Append(LINKFLAGS=["-s", "RELOCATABLE=1"]) + env.extra_suffix = ".gdnative" + env.extra_suffix + # Reduce code size by generating less support code (e.g. skip NodeJS support). env.Append(LINKFLAGS=["-s", "ENVIRONMENT=web,worker"]) - # We use IDBFS in javascript_main.cpp. Since Emscripten 1.39.1 it needs to - # be linked explicitly. - env.Append(LIBS=["idbfs.js"]) - - env.Append(LINKFLAGS=["-s", "BINARYEN=1"]) - env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", 'EXPORT_NAME="Godot"']) + # Wrap the JavaScript support code around a closure named Godot. + env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", "EXPORT_NAME='Godot'"]) # Allow increasing memory buffer size during runtime. This is efficient # when using WebAssembly (in comparison to asm.js) and works well for @@ -160,7 +211,14 @@ def configure(env): # This setting just makes WebGL 2 APIs available, it does NOT disable WebGL 1. env.Append(LINKFLAGS=["-s", "USE_WEBGL2=1"]) + # Do not call main immediately when the support code is ready. env.Append(LINKFLAGS=["-s", "INVOKE_RUN=0"]) - # callMain for manual start, FS for preloading. - env.Append(LINKFLAGS=["-s", 'EXTRA_EXPORTED_RUNTIME_METHODS=["callMain", "FS"]']) + # Allow use to take control of swapping WebGL buffers. + env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"]) + + # callMain for manual start. + env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain']"]) + + # Add code that allow exiting runtime. + env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"]) diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp new file mode 100644 index 0000000000..f10627b0b6 --- /dev/null +++ b/platform/javascript/display_server_javascript.cpp @@ -0,0 +1,1043 @@ +/*************************************************************************/ +/* display_server_javascript.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "platform/javascript/display_server_javascript.h" + +#include "drivers/dummy/rasterizer_dummy.h" +#include "platform/javascript/os_javascript.h" + +#include <emscripten.h> +#include <png.h> + +#include "dom_keys.inc" +#include "godot_js.h" + +#define DOM_BUTTON_LEFT 0 +#define DOM_BUTTON_MIDDLE 1 +#define DOM_BUTTON_RIGHT 2 +#define DOM_BUTTON_XBUTTON1 3 +#define DOM_BUTTON_XBUTTON2 4 + +DisplayServerJavaScript *DisplayServerJavaScript::get_singleton() { + return static_cast<DisplayServerJavaScript *>(DisplayServer::get_singleton()); +} + +// Window (canvas) +void DisplayServerJavaScript::focus_canvas() { + godot_js_display_canvas_focus(); +} + +bool DisplayServerJavaScript::is_canvas_focused() { + return godot_js_display_canvas_is_focused() != 0; +} + +bool DisplayServerJavaScript::check_size_force_redraw() { + int canvas_width; + int canvas_height; + emscripten_get_canvas_element_size(DisplayServerJavaScript::canvas_id, &canvas_width, &canvas_height); + if (last_width != canvas_width || last_height != canvas_height) { + last_width = canvas_width; + last_height = canvas_height; + // Update the framebuffer size for redraw. + emscripten_set_canvas_element_size(DisplayServerJavaScript::canvas_id, canvas_width, canvas_height); + return true; + } + return false; +} + +Point2 DisplayServerJavaScript::compute_position_in_canvas(int p_x, int p_y) { + DisplayServerJavaScript *display = get_singleton(); + int canvas_x; + int canvas_y; + godot_js_display_canvas_bounding_rect_position_get(&canvas_x, &canvas_y); + int canvas_width; + int canvas_height; + emscripten_get_canvas_element_size(display->canvas_id, &canvas_width, &canvas_height); + + double element_width; + double element_height; + emscripten_get_element_css_size(display->canvas_id, &element_width, &element_height); + + return Point2((int)(canvas_width / element_width * (p_x - canvas_x)), + (int)(canvas_height / element_height * (p_y - canvas_y))); +} + +EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) { + DisplayServerJavaScript *display = get_singleton(); + // Empty ID is canvas. + String target_id = String::utf8(p_event->id); + if (target_id.is_empty() || target_id == String::utf8(display->canvas_id)) { + // This event property is the only reliable data on + // browser fullscreen state. + if (p_event->isFullscreen) { + display->window_mode = WINDOW_MODE_FULLSCREEN; + } else { + display->window_mode = WINDOW_MODE_WINDOWED; + } + } + return false; +} + +// Drag and drop callback. +void DisplayServerJavaScript::drop_files_js_callback(char **p_filev, int p_filec) { + DisplayServerJavaScript *ds = get_singleton(); + if (!ds) { + ERR_FAIL_MSG("Unable to drop files because the DisplayServer is not active"); + } + if (ds->drop_files_callback.is_null()) { + return; + } + Vector<String> files; + for (int i = 0; i < p_filec; i++) { + files.push_back(String::utf8(p_filev[i])); + } + Variant v = files; + Variant *vp = &v; + Variant ret; + Callable::CallError ce; + ds->drop_files_callback.call((const Variant **)&vp, 1, ret, ce); +} + +// JavaScript quit request callback. +void DisplayServerJavaScript::request_quit_callback() { + DisplayServerJavaScript *ds = get_singleton(); + if (ds && !ds->window_event_callback.is_null()) { + Variant event = int(DisplayServer::WINDOW_EVENT_CLOSE_REQUEST); + Variant *eventp = &event; + Variant ret; + Callable::CallError ce; + ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce); + } +} + +// Keys + +template <typename T> +void DisplayServerJavaScript::dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event) { + godot_event->set_shift(emscripten_event_ptr->shiftKey); + godot_event->set_alt(emscripten_event_ptr->altKey); + godot_event->set_control(emscripten_event_ptr->ctrlKey); + godot_event->set_metakey(emscripten_event_ptr->metaKey); +} + +Ref<InputEventKey> DisplayServerJavaScript::setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) { + Ref<InputEventKey> ev; + ev.instance(); + ev->set_echo(emscripten_event->repeat); + dom2godot_mod(emscripten_event, ev); + ev->set_keycode(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, false)); + ev->set_physical_keycode(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, true)); + + String unicode = String::utf8(emscripten_event->key); + // Check if empty or multi-character (e.g. `CapsLock`). + if (unicode.length() != 1) { + // Might be empty as well, but better than nonsense. + unicode = String::utf8(emscripten_event->charValue); + } + if (unicode.length() == 1) { + ev->set_unicode(unicode[0]); + } + + return ev; +} + +EM_BOOL DisplayServerJavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { + DisplayServerJavaScript *display = get_singleton(); + Ref<InputEventKey> ev = setup_key_event(p_event); + ev->set_pressed(true); + if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_keycode())) { + // Defer to keypress event for legacy unicode retrieval. + display->deferred_key_event = ev; + // Do not suppress keypress event. + return false; + } + Input::get_singleton()->parse_input_event(ev); + return true; +} + +EM_BOOL DisplayServerJavaScript::keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { + DisplayServerJavaScript *display = get_singleton(); + display->deferred_key_event->set_unicode(p_event->charCode); + Input::get_singleton()->parse_input_event(display->deferred_key_event); + return true; +} + +EM_BOOL DisplayServerJavaScript::keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { + Ref<InputEventKey> ev = setup_key_event(p_event); + ev->set_pressed(false); + Input::get_singleton()->parse_input_event(ev); + return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != 0; +} + +// Mouse + +EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { + DisplayServerJavaScript *display = get_singleton(); + + Ref<InputEventMouseButton> ev; + ev.instance(); + ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_MOUSEDOWN); + ev->set_position(compute_position_in_canvas(p_event->clientX, p_event->clientY)); + ev->set_global_position(ev->get_position()); + dom2godot_mod(p_event, ev); + + switch (p_event->button) { + case DOM_BUTTON_LEFT: + ev->set_button_index(BUTTON_LEFT); + break; + case DOM_BUTTON_MIDDLE: + ev->set_button_index(BUTTON_MIDDLE); + break; + case DOM_BUTTON_RIGHT: + ev->set_button_index(BUTTON_RIGHT); + break; + case DOM_BUTTON_XBUTTON1: + ev->set_button_index(BUTTON_XBUTTON1); + break; + case DOM_BUTTON_XBUTTON2: + ev->set_button_index(BUTTON_XBUTTON2); + break; + default: + return false; + } + + if (ev->is_pressed()) { + double diff = emscripten_get_now() - display->last_click_ms; + + if (ev->get_button_index() == display->last_click_button_index) { + if (diff < 400 && Point2(display->last_click_pos).distance_to(ev->get_position()) < 5) { + display->last_click_ms = 0; + display->last_click_pos = Point2(-100, -100); + display->last_click_button_index = -1; + ev->set_doubleclick(true); + } + + } else { + display->last_click_button_index = ev->get_button_index(); + } + + if (!ev->is_doubleclick()) { + display->last_click_ms += diff; + display->last_click_pos = ev->get_position(); + } + } + + Input *input = Input::get_singleton(); + int mask = input->get_mouse_button_mask(); + int button_flag = 1 << (ev->get_button_index() - 1); + if (ev->is_pressed()) { + // Since the event is consumed, focus manually. The containing iframe, + // if exists, may not have focus yet, so focus even if already focused. + focus_canvas(); + mask |= button_flag; + } else if (mask & button_flag) { + mask &= ~button_flag; + } else { + // Received release event, but press was outside the canvas, so ignore. + return false; + } + ev->set_button_mask(mask); + + input->parse_input_event(ev); + // Prevent multi-click text selection and wheel-click scrolling anchor. + // Context menu is prevented through contextmenu event. + return true; +} + +EM_BOOL DisplayServerJavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { + DisplayServerJavaScript *ds = get_singleton(); + Input *input = Input::get_singleton(); + int input_mask = input->get_mouse_button_mask(); + Point2 pos = compute_position_in_canvas(p_event->clientX, p_event->clientY); + // For motion outside the canvas, only read mouse movement if dragging + // started inside the canvas; imitating desktop app behaviour. + if (!ds->cursor_inside_canvas && !input_mask) + return false; + + Ref<InputEventMouseMotion> ev; + ev.instance(); + dom2godot_mod(p_event, ev); + ev->set_button_mask(input_mask); + + ev->set_position(pos); + ev->set_global_position(ev->get_position()); + + ev->set_relative(Vector2(p_event->movementX, p_event->movementY)); + input->set_mouse_position(ev->get_position()); + ev->set_speed(input->get_last_mouse_speed()); + + input->parse_input_event(ev); + // Don't suppress mouseover/-leave events. + return false; +} + +// Cursor +const char *DisplayServerJavaScript::godot2dom_cursor(DisplayServer::CursorShape p_shape) { + switch (p_shape) { + case DisplayServer::CURSOR_ARROW: + return "auto"; + case DisplayServer::CURSOR_IBEAM: + return "text"; + case DisplayServer::CURSOR_POINTING_HAND: + return "pointer"; + case DisplayServer::CURSOR_CROSS: + return "crosshair"; + case DisplayServer::CURSOR_WAIT: + return "progress"; + case DisplayServer::CURSOR_BUSY: + return "wait"; + case DisplayServer::CURSOR_DRAG: + return "grab"; + case DisplayServer::CURSOR_CAN_DROP: + return "grabbing"; + case DisplayServer::CURSOR_FORBIDDEN: + return "no-drop"; + case DisplayServer::CURSOR_VSIZE: + return "ns-resize"; + case DisplayServer::CURSOR_HSIZE: + return "ew-resize"; + case DisplayServer::CURSOR_BDIAGSIZE: + return "nesw-resize"; + case DisplayServer::CURSOR_FDIAGSIZE: + return "nwse-resize"; + case DisplayServer::CURSOR_MOVE: + return "move"; + case DisplayServer::CURSOR_VSPLIT: + return "row-resize"; + case DisplayServer::CURSOR_HSPLIT: + return "col-resize"; + case DisplayServer::CURSOR_HELP: + return "help"; + default: + return "auto"; + } +} + +void DisplayServerJavaScript::cursor_set_shape(CursorShape p_shape) { + ERR_FAIL_INDEX(p_shape, CURSOR_MAX); + if (cursor_shape == p_shape) { + return; + } + cursor_shape = p_shape; + godot_js_display_cursor_set_shape(godot2dom_cursor(cursor_shape)); +} + +DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const { + return cursor_shape; +} + +void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { + if (p_cursor.is_valid()) { + Ref<Texture2D> texture = p_cursor; + Ref<AtlasTexture> atlas_texture = p_cursor; + Ref<Image> image; + Size2 texture_size; + Rect2 atlas_rect; + + if (texture.is_valid()) { + image = texture->get_data(); + } + + if (!image.is_valid() && atlas_texture.is_valid()) { + texture = atlas_texture->get_atlas(); + + atlas_rect.size.width = texture->get_width(); + atlas_rect.size.height = texture->get_height(); + atlas_rect.position.x = atlas_texture->get_region().position.x; + atlas_rect.position.y = atlas_texture->get_region().position.y; + + texture_size.width = atlas_texture->get_region().size.x; + texture_size.height = atlas_texture->get_region().size.y; + } else if (image.is_valid()) { + texture_size.width = texture->get_width(); + texture_size.height = texture->get_height(); + } + + ERR_FAIL_COND(!texture.is_valid()); + ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); + ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); + ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); + + image = texture->get_data(); + + ERR_FAIL_COND(!image.is_valid()); + + image = image->duplicate(); + + if (atlas_texture.is_valid()) + image->crop_from_point( + atlas_rect.position.x, + atlas_rect.position.y, + texture_size.width, + texture_size.height); + + if (image->get_format() != Image::FORMAT_RGBA8) { + image->convert(Image::FORMAT_RGBA8); + } + + png_image png_meta; + memset(&png_meta, 0, sizeof png_meta); + png_meta.version = PNG_IMAGE_VERSION; + png_meta.width = texture_size.width; + png_meta.height = texture_size.height; + png_meta.format = PNG_FORMAT_RGBA; + + PackedByteArray png; + size_t len; + PackedByteArray data = image->get_data(); + ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); + + png.resize(len); + ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); + + godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), png.ptr(), len, p_hotspot.x, p_hotspot.y); + + } else { + godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), NULL, 0, 0, 0); + } + + cursor_set_shape(cursor_shape); +} + +// Mouse mode +void DisplayServerJavaScript::mouse_set_mode(MouseMode p_mode) { + ERR_FAIL_COND_MSG(p_mode == MOUSE_MODE_CONFINED, "MOUSE_MODE_CONFINED is not supported for the HTML5 platform."); + if (p_mode == mouse_get_mode()) + return; + + if (p_mode == MOUSE_MODE_VISIBLE) { + godot_js_display_cursor_set_visible(1); + emscripten_exit_pointerlock(); + + } else if (p_mode == MOUSE_MODE_HIDDEN) { + godot_js_display_cursor_set_visible(0); + emscripten_exit_pointerlock(); + + } else if (p_mode == MOUSE_MODE_CAPTURED) { + godot_js_display_cursor_set_visible(1); + EMSCRIPTEN_RESULT result = emscripten_request_pointerlock(canvas_id, false); + ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); + ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); + } +} + +DisplayServer::MouseMode DisplayServerJavaScript::mouse_get_mode() const { + if (godot_js_display_cursor_is_hidden()) { + return MOUSE_MODE_HIDDEN; + } + + EmscriptenPointerlockChangeEvent ev; + emscripten_get_pointerlock_status(&ev); + return (ev.isActive && String::utf8(ev.id) == String::utf8(canvas_id)) ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; +} + +// Wheel +EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data) { + ERR_FAIL_COND_V(p_event_type != EMSCRIPTEN_EVENT_WHEEL, false); + DisplayServerJavaScript *ds = get_singleton(); + if (!is_canvas_focused()) { + if (ds->cursor_inside_canvas) { + focus_canvas(); + } else { + return false; + } + } + + Input *input = Input::get_singleton(); + Ref<InputEventMouseButton> ev; + ev.instance(); + ev->set_position(input->get_mouse_position()); + ev->set_global_position(ev->get_position()); + + ev->set_shift(input->is_key_pressed(KEY_SHIFT)); + ev->set_alt(input->is_key_pressed(KEY_ALT)); + ev->set_control(input->is_key_pressed(KEY_CONTROL)); + ev->set_metakey(input->is_key_pressed(KEY_META)); + + if (p_event->deltaY < 0) + ev->set_button_index(BUTTON_WHEEL_UP); + else if (p_event->deltaY > 0) + ev->set_button_index(BUTTON_WHEEL_DOWN); + else if (p_event->deltaX > 0) + ev->set_button_index(BUTTON_WHEEL_LEFT); + else if (p_event->deltaX < 0) + ev->set_button_index(BUTTON_WHEEL_RIGHT); + else + return false; + + // Different browsers give wildly different delta values, and we can't + // interpret deltaMode, so use default value for wheel events' factor. + + int button_flag = 1 << (ev->get_button_index() - 1); + + ev->set_pressed(true); + ev->set_button_mask(input->get_mouse_button_mask() | button_flag); + input->parse_input_event(ev); + + ev->set_pressed(false); + ev->set_button_mask(input->get_mouse_button_mask() & ~button_flag); + input->parse_input_event(ev); + + return true; +} + +// Touch +EM_BOOL DisplayServerJavaScript::touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { + DisplayServerJavaScript *display = get_singleton(); + Ref<InputEventScreenTouch> ev; + ev.instance(); + int lowest_id_index = -1; + for (int i = 0; i < p_event->numTouches; ++i) { + const EmscriptenTouchPoint &touch = p_event->touches[i]; + if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) + lowest_id_index = i; + if (!touch.isChanged) + continue; + ev->set_index(touch.identifier); + ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); + display->touches[i] = ev->get_position(); + ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_TOUCHSTART); + + Input::get_singleton()->parse_input_event(ev); + } + // Resume audio context after input in case autoplay was denied. + return true; +} + +EM_BOOL DisplayServerJavaScript::touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { + DisplayServerJavaScript *display = get_singleton(); + Ref<InputEventScreenDrag> ev; + ev.instance(); + int lowest_id_index = -1; + for (int i = 0; i < p_event->numTouches; ++i) { + const EmscriptenTouchPoint &touch = p_event->touches[i]; + if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) + lowest_id_index = i; + if (!touch.isChanged) + continue; + ev->set_index(touch.identifier); + ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); + Point2 &prev = display->touches[i]; + ev->set_relative(ev->get_position() - prev); + prev = ev->get_position(); + + Input::get_singleton()->parse_input_event(ev); + } + return true; +} + +bool DisplayServerJavaScript::screen_is_touchscreen(int p_screen) const { + return godot_js_display_touchscreen_is_available(); +} + +// Gamepad + +EM_BOOL DisplayServerJavaScript::gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data) { + Input *input = Input::get_singleton(); + if (p_event_type == EMSCRIPTEN_EVENT_GAMEPADCONNECTED) { + String guid = ""; + if (String::utf8(p_event->mapping) == "standard") + guid = "Default HTML5 Gamepad"; + input->joy_connection_changed(p_event->index, true, String::utf8(p_event->id), guid); + } else { + input->joy_connection_changed(p_event->index, false, ""); + } + return true; +} + +void DisplayServerJavaScript::process_joypads() { + int joypad_count = emscripten_get_num_gamepads(); + Input *input = Input::get_singleton(); + for (int joypad = 0; joypad < joypad_count; joypad++) { + EmscriptenGamepadEvent state; + EMSCRIPTEN_RESULT query_result = emscripten_get_gamepad_status(joypad, &state); + // Chromium reserves gamepads slots, so NO_DATA is an expected result. + ERR_CONTINUE(query_result != EMSCRIPTEN_RESULT_SUCCESS && + query_result != EMSCRIPTEN_RESULT_NO_DATA); + if (query_result == EMSCRIPTEN_RESULT_SUCCESS && state.connected) { + int button_count = MIN(state.numButtons, 18); + int axis_count = MIN(state.numAxes, 8); + for (int button = 0; button < button_count; button++) { + float value = state.analogButton[button]; + input->joy_button(joypad, button, value); + } + for (int axis = 0; axis < axis_count; axis++) { + Input::JoyAxis joy_axis; + joy_axis.min = -1; + joy_axis.value = state.axis[axis]; + input->joy_axis(joypad, axis, joy_axis); + } + } + } +} + +#if 0 +bool DisplayServerJavaScript::is_joy_known(int p_device) { + return Input::get_singleton()->is_joy_mapped(p_device); +} + + +String DisplayServerJavaScript::get_joy_guid(int p_device) const { + return Input::get_singleton()->get_joy_guid_remapped(p_device); +} +#endif + +Vector<String> DisplayServerJavaScript::get_rendering_drivers_func() { + Vector<String> drivers; + drivers.push_back("dummy"); + return drivers; +} + +// Clipboard +void DisplayServerJavaScript::update_clipboard_callback(const char *p_text) { + get_singleton()->clipboard = p_text; +} + +void DisplayServerJavaScript::clipboard_set(const String &p_text) { + clipboard = p_text; + int err = godot_js_display_clipboard_set(p_text.utf8().get_data()); + ERR_FAIL_COND_MSG(err, "Clipboard API is not supported."); +} + +String DisplayServerJavaScript::clipboard_get() const { + godot_js_display_clipboard_get(update_clipboard_callback); + return clipboard; +} + +void DisplayServerJavaScript::send_window_event_callback(int p_notification) { + DisplayServerJavaScript *ds = get_singleton(); + if (!ds) { + return; + } + if (p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER || p_notification == DisplayServer::WINDOW_EVENT_MOUSE_EXIT) { + ds->cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER; + } + if (!ds->window_event_callback.is_null()) { + Variant event = int(p_notification); + Variant *eventp = &event; + Variant ret; + Callable::CallError ce; + ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce); + } +} + +void DisplayServerJavaScript::alert(const String &p_alert, const String &p_title) { + godot_js_display_alert(p_alert.utf8().get_data()); +} + +void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) { + ERR_FAIL_COND(p_icon.is_null()); + Ref<Image> icon = p_icon; + if (icon->is_compressed()) { + icon = icon->duplicate(); + ERR_FAIL_COND(icon->decompress() != OK); + } + if (icon->get_format() != Image::FORMAT_RGBA8) { + if (icon == p_icon) + icon = icon->duplicate(); + icon->convert(Image::FORMAT_RGBA8); + } + + png_image png_meta; + memset(&png_meta, 0, sizeof png_meta); + png_meta.version = PNG_IMAGE_VERSION; + png_meta.width = icon->get_width(); + png_meta.height = icon->get_height(); + png_meta.format = PNG_FORMAT_RGBA; + + PackedByteArray png; + size_t len; + PackedByteArray data = icon->get_data(); + ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); + + png.resize(len); + ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); + + godot_js_display_window_icon_set(png.ptr(), len); +} + +void DisplayServerJavaScript::_dispatch_input_event(const Ref<InputEvent> &p_event) { + OS_JavaScript *os = OS_JavaScript::get_singleton(); + + // Resume audio context after input in case autoplay was denied. + os->resume_audio(); + + Callable cb = get_singleton()->input_event_callback; + if (!cb.is_null()) { + Variant ev = p_event; + Variant *evp = &ev; + Variant ret; + Callable::CallError ce; + cb.call((const Variant **)&evp, 1, ret, ce); + } +} + +DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + return memnew(DisplayServerJavaScript(p_rendering_driver, p_mode, p_flags, p_resolution, r_error)); +} + +DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + r_error = OK; // Always succeeds for now. + + // Ensure the canvas ID. + godot_js_config_canvas_id_get(canvas_id, 256); + + // Check if it's windows. + swap_cancel_ok = godot_js_display_is_swap_ok_cancel() == 1; + + // Expose method for requesting quit. + godot_js_os_request_quit_cb(request_quit_callback); + + RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? +#if 0 + EmscriptenWebGLContextAttributes attributes; + emscripten_webgl_init_context_attributes(&attributes); + attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed"); + attributes.antialias = false; + ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER); + + if (p_desired.layered) { + set_window_per_pixel_transparency_enabled(true); + } + + bool gl_initialization_error = false; + + if (RasterizerGLES2::is_viable() == OK) { + attributes.majorVersion = 1; + RasterizerGLES2::register_config(); + RasterizerGLES2::make_current(); + } else { + gl_initialization_error = true; + } + + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(canvas_id, &attributes); + if (emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS) { + gl_initialization_error = true; + } + + if (gl_initialization_error) { + OS::get_singleton()->alert("Your browser does not seem to support WebGL. Please update your browser version.", + "Unable to initialize video driver"); + return ERR_UNAVAILABLE; + } + + video_driver_index = p_video_driver; +#endif + + window_set_mode(p_mode); + if (godot_js_config_is_resize_on_start()) { + window_set_size(p_resolution); + } + + EMSCRIPTEN_RESULT result; +#define EM_CHECK(ev) \ + if (result != EMSCRIPTEN_RESULT_SUCCESS) \ + ERR_PRINT("Error while setting " #ev " callback: Code " + itos(result)); +#define SET_EM_CALLBACK(target, ev, cb) \ + result = emscripten_set_##ev##_callback(target, nullptr, true, &cb); \ + EM_CHECK(ev) +#define SET_EM_WINDOW_CALLBACK(ev, cb) \ + result = emscripten_set_##ev##_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, &cb); \ + EM_CHECK(ev) +#define SET_EM_CALLBACK_NOTARGET(ev, cb) \ + result = emscripten_set_##ev##_callback(nullptr, true, &cb); \ + EM_CHECK(ev) + // These callbacks from Emscripten's html5.h suffice to access most + // JavaScript APIs. + SET_EM_CALLBACK(canvas_id, mousedown, mouse_button_callback) + SET_EM_WINDOW_CALLBACK(mousemove, mousemove_callback) + SET_EM_WINDOW_CALLBACK(mouseup, mouse_button_callback) + SET_EM_CALLBACK(canvas_id, wheel, wheel_callback) + SET_EM_CALLBACK(canvas_id, touchstart, touch_press_callback) + SET_EM_CALLBACK(canvas_id, touchmove, touchmove_callback) + SET_EM_CALLBACK(canvas_id, touchend, touch_press_callback) + SET_EM_CALLBACK(canvas_id, touchcancel, touch_press_callback) + SET_EM_CALLBACK(canvas_id, keydown, keydown_callback) + SET_EM_CALLBACK(canvas_id, keypress, keypress_callback) + SET_EM_CALLBACK(canvas_id, keyup, keyup_callback) + SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback) + SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback) + SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback) +#undef SET_EM_CALLBACK_NOTARGET +#undef SET_EM_CALLBACK +#undef EM_CHECK + + // For APIs that are not (sufficiently) exposed, a + // library is used below (implemented in library_godot_display.js). + godot_js_display_notification_cb(&send_window_event_callback, + WINDOW_EVENT_MOUSE_ENTER, + WINDOW_EVENT_MOUSE_EXIT, + WINDOW_EVENT_FOCUS_IN, + WINDOW_EVENT_FOCUS_OUT); + godot_js_display_paste_cb(update_clipboard_callback); + godot_js_display_drop_files_cb(drop_files_js_callback); + + Input::get_singleton()->set_event_dispatch_function(_dispatch_input_event); +} + +DisplayServerJavaScript::~DisplayServerJavaScript() { + //emscripten_webgl_commit_frame(); + //emscripten_webgl_destroy_context(webgl_ctx); +} + +bool DisplayServerJavaScript::has_feature(Feature p_feature) const { + switch (p_feature) { + //case FEATURE_CONSOLE_WINDOW: + //case FEATURE_GLOBAL_MENU: + //case FEATURE_HIDPI: + //case FEATURE_IME: + case FEATURE_ICON: + case FEATURE_CLIPBOARD: + case FEATURE_CURSOR_SHAPE: + case FEATURE_CUSTOM_CURSOR_SHAPE: + case FEATURE_MOUSE: + case FEATURE_TOUCHSCREEN: + return true; + //case FEATURE_MOUSE_WARP: + //case FEATURE_NATIVE_DIALOG: + //case FEATURE_NATIVE_ICON: + //case FEATURE_NATIVE_VIDEO: + //case FEATURE_WINDOW_TRANSPARENCY: + //case FEATURE_KEEP_SCREEN_ON: + //case FEATURE_ORIENTATION: + //case FEATURE_VIRTUAL_KEYBOARD: + default: + return false; + } +} + +void DisplayServerJavaScript::register_javascript_driver() { + register_create_function("javascript", create_func, get_rendering_drivers_func); +} + +String DisplayServerJavaScript::get_name() const { + return "javascript"; +} + +int DisplayServerJavaScript::get_screen_count() const { + return 1; +} + +Point2i DisplayServerJavaScript::screen_get_position(int p_screen) const { + return Point2i(); // TODO offsetX/Y? +} + +Size2i DisplayServerJavaScript::screen_get_size(int p_screen) const { + EmscriptenFullscreenChangeEvent ev; + EMSCRIPTEN_RESULT result = emscripten_get_fullscreen_status(&ev); + ERR_FAIL_COND_V(result != EMSCRIPTEN_RESULT_SUCCESS, Size2i()); + return Size2i(ev.screenWidth, ev.screenHeight); +} + +Rect2i DisplayServerJavaScript::screen_get_usable_rect(int p_screen) const { + int canvas[2]; + emscripten_get_canvas_element_size(canvas_id, canvas, canvas + 1); + return Rect2i(0, 0, canvas[0], canvas[1]); +} + +int DisplayServerJavaScript::screen_get_dpi(int p_screen) const { + return 96; // TODO maybe check pixel ratio via window.devicePixelRatio * 96? Inexact. +} + +Vector<DisplayServer::WindowID> DisplayServerJavaScript::get_window_list() const { + Vector<WindowID> ret; + ret.push_back(MAIN_WINDOW_ID); + return ret; +} + +DisplayServerJavaScript::WindowID DisplayServerJavaScript::get_window_at_screen_position(const Point2i &p_position) const { + return MAIN_WINDOW_ID; +} + +void DisplayServerJavaScript::window_attach_instance_id(ObjectID p_instance, WindowID p_window) { + window_attached_instance_id = p_instance; +} + +ObjectID DisplayServerJavaScript::window_get_attached_instance_id(WindowID p_window) const { + return window_attached_instance_id; +} + +void DisplayServerJavaScript::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) { + // Not supported. +} + +void DisplayServerJavaScript::window_set_window_event_callback(const Callable &p_callable, WindowID p_window) { + window_event_callback = p_callable; +} + +void DisplayServerJavaScript::window_set_input_event_callback(const Callable &p_callable, WindowID p_window) { + input_event_callback = p_callable; +} + +void DisplayServerJavaScript::window_set_input_text_callback(const Callable &p_callable, WindowID p_window) { + input_text_callback = p_callable; // TODO unused... do I need this? +} + +void DisplayServerJavaScript::window_set_drop_files_callback(const Callable &p_callable, WindowID p_window) { + drop_files_callback = p_callable; +} + +void DisplayServerJavaScript::window_set_title(const String &p_title, WindowID p_window) { + godot_js_display_window_title_set(p_title.utf8().get_data()); +} + +int DisplayServerJavaScript::window_get_current_screen(WindowID p_window) const { + return 1; +} + +void DisplayServerJavaScript::window_set_current_screen(int p_screen, WindowID p_window) { + // Not implemented. +} + +Point2i DisplayServerJavaScript::window_get_position(WindowID p_window) const { + return Point2i(); // TODO Does this need implementation? +} + +void DisplayServerJavaScript::window_set_position(const Point2i &p_position, WindowID p_window) { + // Not supported. +} + +void DisplayServerJavaScript::window_set_transient(WindowID p_window, WindowID p_parent) { + // Not supported. +} + +void DisplayServerJavaScript::window_set_max_size(const Size2i p_size, WindowID p_window) { + // Not supported. +} + +Size2i DisplayServerJavaScript::window_get_max_size(WindowID p_window) const { + return Size2i(); +} + +void DisplayServerJavaScript::window_set_min_size(const Size2i p_size, WindowID p_window) { + // Not supported. +} + +Size2i DisplayServerJavaScript::window_get_min_size(WindowID p_window) const { + return Size2i(); +} + +void DisplayServerJavaScript::window_set_size(const Size2i p_size, WindowID p_window) { + last_width = p_size.x; + last_height = p_size.y; + double scale = godot_js_display_pixel_ratio_get(); + emscripten_set_canvas_element_size(canvas_id, p_size.x, p_size.y); + emscripten_set_element_css_size(canvas_id, p_size.x / scale, p_size.y / scale); +} + +Size2i DisplayServerJavaScript::window_get_size(WindowID p_window) const { + int canvas[2]; + emscripten_get_canvas_element_size(canvas_id, canvas, canvas + 1); + return Size2(canvas[0], canvas[1]); +} + +Size2i DisplayServerJavaScript::window_get_real_size(WindowID p_window) const { + return window_get_size(p_window); +} + +void DisplayServerJavaScript::window_set_mode(WindowMode p_mode, WindowID p_window) { + if (window_mode == p_mode) + return; + + switch (p_mode) { + case WINDOW_MODE_WINDOWED: { + if (window_mode == WINDOW_MODE_FULLSCREEN) { + emscripten_exit_fullscreen(); + } + window_mode = WINDOW_MODE_WINDOWED; + window_set_size(Size2i(last_width, last_height)); + } break; + case WINDOW_MODE_FULLSCREEN: { + EmscriptenFullscreenStrategy strategy; + strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; + strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; + strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; + strategy.canvasResizedCallback = nullptr; + EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(canvas_id, false, &strategy); + ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "Enabling fullscreen is only possible from an input callback for the HTML5 platform."); + ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "Enabling fullscreen is only possible from an input callback for the HTML5 platform."); + } break; + case WINDOW_MODE_MAXIMIZED: + case WINDOW_MODE_MINIMIZED: + WARN_PRINT("WindowMode MAXIMIZED and MINIMIZED are not supported in HTML5 platform."); + break; + default: + break; + } +} + +DisplayServerJavaScript::WindowMode DisplayServerJavaScript::window_get_mode(WindowID p_window) const { + return window_mode; +} + +bool DisplayServerJavaScript::window_is_maximize_allowed(WindowID p_window) const { + return false; +} + +void DisplayServerJavaScript::window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window) { + // Not supported. +} + +bool DisplayServerJavaScript::window_get_flag(WindowFlags p_flag, WindowID p_window) const { + return false; +} + +void DisplayServerJavaScript::window_request_attention(WindowID p_window) { + // Not supported. +} + +void DisplayServerJavaScript::window_move_to_foreground(WindowID p_window) { + // Not supported. +} + +bool DisplayServerJavaScript::window_can_draw(WindowID p_window) const { + return true; +} + +bool DisplayServerJavaScript::can_any_window_draw() const { + return true; +} + +void DisplayServerJavaScript::process_events() { + if (emscripten_sample_gamepad_data() == EMSCRIPTEN_RESULT_SUCCESS) + process_joypads(); +} + +int DisplayServerJavaScript::get_current_video_driver() const { + return 1; +} + +bool DisplayServerJavaScript::get_swap_cancel_ok() { + return swap_cancel_ok; +} + +void DisplayServerJavaScript::swap_buffers() { + //emscripten_webgl_commit_frame(); +} diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h new file mode 100644 index 0000000000..916be1ae45 --- /dev/null +++ b/platform/javascript/display_server_javascript.h @@ -0,0 +1,205 @@ +/*************************************************************************/ +/* display_server_javascript.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef DISPLAY_SERVER_JAVASCRIPT_H +#define DISPLAY_SERVER_JAVASCRIPT_H + +#include "servers/display_server.h" + +#include <emscripten.h> +#include <emscripten/html5.h> + +class DisplayServerJavaScript : public DisplayServer { +private: + WindowMode window_mode = WINDOW_MODE_WINDOWED; + ObjectID window_attached_instance_id = {}; + + Callable window_event_callback; + Callable input_event_callback; + Callable input_text_callback; + Callable drop_files_callback; + + String clipboard; + Ref<InputEventKey> deferred_key_event; + Point2 touches[32]; + + char canvas_id[256] = { 0 }; + bool cursor_inside_canvas = true; + CursorShape cursor_shape = CURSOR_ARROW; + Point2i last_click_pos = Point2(-100, -100); // TODO check this again. + double last_click_ms = 0; + int last_click_button_index = -1; + + int last_width = 0; + int last_height = 0; + + bool swap_cancel_ok = false; + + // utilities + static Point2 compute_position_in_canvas(int p_x, int p_y); + static void focus_canvas(); + static bool is_canvas_focused(); + template <typename T> + static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event); + static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event); + static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape); + + // events + static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data); + + static EM_BOOL keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); + static EM_BOOL keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); + static EM_BOOL keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); + + static EM_BOOL mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); + static EM_BOOL mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); + + static EM_BOOL wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data); + + static EM_BOOL touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); + static EM_BOOL touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); + + static EM_BOOL gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data); + void process_joypads(); + + static Vector<String> get_rendering_drivers_func(); + static DisplayServer *create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + + static void _dispatch_input_event(const Ref<InputEvent> &p_event); + + static void request_quit_callback(); + static void update_clipboard_callback(const char *p_text); + static void send_window_event_callback(int p_notification); + static void drop_files_js_callback(char **p_filev, int p_filec); + +protected: + int get_current_video_driver() const; + +public: + // Override return type to make writing static callbacks less tedious. + static DisplayServerJavaScript *get_singleton(); + + // utilities + bool check_size_force_redraw(); + + // from DisplayServer + void alert(const String &p_alert, const String &p_title = "ALERT!") override; + bool has_feature(Feature p_feature) const override; + String get_name() const override; + + // cursor + void cursor_set_shape(CursorShape p_shape) override; + CursorShape cursor_get_shape() const override; + void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; + + // mouse + void mouse_set_mode(MouseMode p_mode) override; + MouseMode mouse_get_mode() const override; + + // touch + bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + + // clipboard + void clipboard_set(const String &p_text) override; + String clipboard_get() const override; + + // screen + int get_screen_count() const override; + Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + + // windows + Vector<DisplayServer::WindowID> get_window_list() const override; + WindowID get_window_at_screen_position(const Point2i &p_position) const override; + + void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override; + ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override; + + void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + + void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + + void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + + void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override; + + int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override; + void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID) override; + + Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const override; + void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID) override; + + void window_set_transient(WindowID p_window, WindowID p_parent) override; + + void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const override; + + void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const override; + + void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override; + Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const override; + + void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) override; + WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const override; + + bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const override; + + void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID) override; + bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const override; + + void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override; + void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override; + + bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override; + + bool can_any_window_draw() const override; + + // events + void process_events() override; + + // icon + void set_icon(const Ref<Image> &p_icon) override; + + // others + bool get_swap_cancel_ok() override; + void swap_buffers() override; + + static void register_javascript_driver(); + DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + ~DisplayServerJavaScript(); +}; + +#endif // DISPLAY_SERVER_JAVASCRIPT_H diff --git a/platform/javascript/dom_keys.inc b/platform/javascript/dom_keys.inc index fd9df765d2..7902efafe0 100644 --- a/platform/javascript/dom_keys.inc +++ b/platform/javascript/dom_keys.inc @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,351 +30,203 @@ #include "core/os/keyboard.h" -// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#Constants_for_keyCode_value -#define DOM_VK_CANCEL 0x03 -#define DOM_VK_HELP 0x06 -#define DOM_VK_BACK_SPACE 0x08 -#define DOM_VK_TAB 0x09 -#define DOM_VK_CLEAR 0x0C -#define DOM_VK_RETURN 0x0D -#define DOM_VK_ENTER 0x0E // "Reserved, but not used." -#define DOM_VK_SHIFT 0x10 -#define DOM_VK_CONTROL 0x11 -#define DOM_VK_ALT 0x12 -#define DOM_VK_PAUSE 0x13 -#define DOM_VK_CAPS_LOCK 0x14 -#define DOM_VK_KANA 0x15 -#define DOM_VK_HANGUL 0x15 -#define DOM_VK_EISU 0x16 -#define DOM_VK_JUNJA 0x17 -#define DOM_VK_FINAL 0x18 -#define DOM_VK_HANJA 0x19 -#define DOM_VK_KANJI 0x19 -#define DOM_VK_ESCAPE 0x1B -#define DOM_VK_CONVERT 0x1C -#define DOM_VK_NONCONVERT 0x1D -#define DOM_VK_ACCEPT 0x1E -#define DOM_VK_MODECHANGE 0x1F -#define DOM_VK_SPACE 0x20 -#define DOM_VK_PAGE_UP 0x21 -#define DOM_VK_PAGE_DOWN 0x22 -#define DOM_VK_END 0x23 -#define DOM_VK_HOME 0x24 -#define DOM_VK_LEFT 0x25 -#define DOM_VK_UP 0x26 -#define DOM_VK_RIGHT 0x27 -#define DOM_VK_DOWN 0x28 -#define DOM_VK_SELECT 0x29 -#define DOM_VK_PRINT 0x2A -#define DOM_VK_EXECUTE 0x2B -#define DOM_VK_PRINTSCREEN 0x2C -#define DOM_VK_INSERT 0x2D -#define DOM_VK_DELETE 0x2E -#define DOM_VK_0 0x30 -#define DOM_VK_1 0x31 -#define DOM_VK_2 0x32 -#define DOM_VK_3 0x33 -#define DOM_VK_4 0x34 -#define DOM_VK_5 0x35 -#define DOM_VK_6 0x36 -#define DOM_VK_7 0x37 -#define DOM_VK_8 0x38 -#define DOM_VK_9 0x39 -#define DOM_VK_COLON 0x3A -#define DOM_VK_SEMICOLON 0x3B -#define DOM_VK_LESS_THAN 0x3C -#define DOM_VK_EQUALS 0x3D -#define DOM_VK_GREATER_THAN 0x3E -#define DOM_VK_QUESTION_MARK 0x3F -#define DOM_VK_AT 0x40 -#define DOM_VK_A 0x41 -#define DOM_VK_B 0x42 -#define DOM_VK_C 0x43 -#define DOM_VK_D 0x44 -#define DOM_VK_E 0x45 -#define DOM_VK_F 0x46 -#define DOM_VK_G 0x47 -#define DOM_VK_H 0x48 -#define DOM_VK_I 0x49 -#define DOM_VK_J 0x4A -#define DOM_VK_K 0x4B -#define DOM_VK_L 0x4C -#define DOM_VK_M 0x4D -#define DOM_VK_N 0x4E -#define DOM_VK_O 0x4F -#define DOM_VK_P 0x50 -#define DOM_VK_Q 0x51 -#define DOM_VK_R 0x52 -#define DOM_VK_S 0x53 -#define DOM_VK_T 0x54 -#define DOM_VK_U 0x55 -#define DOM_VK_V 0x56 -#define DOM_VK_W 0x57 -#define DOM_VK_X 0x58 -#define DOM_VK_Y 0x59 -#define DOM_VK_Z 0x5A -#define DOM_VK_WIN 0x5B -#define DOM_VK_CONTEXT_MENU 0x5D -#define DOM_VK_SLEEP 0x5F -#define DOM_VK_NUMPAD0 0x60 -#define DOM_VK_NUMPAD1 0x61 -#define DOM_VK_NUMPAD2 0x62 -#define DOM_VK_NUMPAD3 0x63 -#define DOM_VK_NUMPAD4 0x64 -#define DOM_VK_NUMPAD5 0x65 -#define DOM_VK_NUMPAD6 0x66 -#define DOM_VK_NUMPAD7 0x67 -#define DOM_VK_NUMPAD8 0x68 -#define DOM_VK_NUMPAD9 0x69 -#define DOM_VK_MULTIPLY 0x6A -#define DOM_VK_ADD 0x6B -#define DOM_VK_SEPARATOR 0x6C -#define DOM_VK_SUBTRACT 0x6D -#define DOM_VK_DECIMAL 0x6E -#define DOM_VK_DIVIDE 0x6F -#define DOM_VK_F1 0x70 -#define DOM_VK_F2 0x71 -#define DOM_VK_F3 0x72 -#define DOM_VK_F4 0x73 -#define DOM_VK_F5 0x74 -#define DOM_VK_F6 0x75 -#define DOM_VK_F7 0x76 -#define DOM_VK_F8 0x77 -#define DOM_VK_F9 0x78 -#define DOM_VK_F10 0x79 -#define DOM_VK_F11 0x7A -#define DOM_VK_F12 0x7B -#define DOM_VK_F13 0x7C -#define DOM_VK_F14 0x7D -#define DOM_VK_F15 0x7E -#define DOM_VK_F16 0x7F -#define DOM_VK_F17 0x80 -#define DOM_VK_F18 0x81 -#define DOM_VK_F19 0x82 -#define DOM_VK_F20 0x83 -#define DOM_VK_F21 0x84 -#define DOM_VK_F22 0x85 -#define DOM_VK_F23 0x86 -#define DOM_VK_F24 0x87 -#define DOM_VK_NUM_LOCK 0x90 -#define DOM_VK_SCROLL_LOCK 0x91 -#define DOM_VK_WIN_OEM_FJ_JISHO 0x92 -#define DOM_VK_WIN_OEM_FJ_MASSHOU 0x93 -#define DOM_VK_WIN_OEM_FJ_TOUROKU 0x94 -#define DOM_VK_WIN_OEM_FJ_LOYA 0x95 -#define DOM_VK_WIN_OEM_FJ_ROYA 0x96 -#define DOM_VK_CIRCUMFLEX 0xA0 -#define DOM_VK_EXCLAMATION 0xA1 -#define DOM_VK_DOUBLE_QUOTE 0xA2 -#define DOM_VK_HASH 0xA3 -#define DOM_VK_DOLLAR 0xA4 -#define DOM_VK_PERCENT 0xA5 -#define DOM_VK_AMPERSAND 0xA6 -#define DOM_VK_UNDERSCORE 0xA7 -#define DOM_VK_OPEN_PAREN 0xA8 -#define DOM_VK_CLOSE_PAREN 0xA9 -#define DOM_VK_ASTERISK 0xAA -#define DOM_VK_PLUS 0xAB -#define DOM_VK_PIPE 0xAC -#define DOM_VK_HYPHEN_MINUS 0xAD -#define DOM_VK_OPEN_CURLY_BRACKET 0xAE -#define DOM_VK_CLOSE_CURLY_BRACKET 0xAF -#define DOM_VK_TILDE 0xB0 -#define DOM_VK_VOLUME_MUTE 0xB5 -#define DOM_VK_VOLUME_DOWN 0xB6 -#define DOM_VK_VOLUME_UP 0xB7 -#define DOM_VK_COMMA 0xBC -#define DOM_VK_PERIOD 0xBE -#define DOM_VK_SLASH 0xBF -#define DOM_VK_BACK_QUOTE 0xC0 -#define DOM_VK_OPEN_BRACKET 0xDB -#define DOM_VK_BACK_SLASH 0xDC -#define DOM_VK_CLOSE_BRACKET 0xDD -#define DOM_VK_QUOTE 0xDE -#define DOM_VK_META 0xE0 -#define DOM_VK_ALTGR 0xE1 -#define DOM_VK_WIN_ICO_HELP 0xE3 -#define DOM_VK_WIN_ICO_00 0xE4 -#define DOM_VK_WIN_ICO_CLEAR 0xE6 -#define DOM_VK_WIN_OEM_RESET 0xE9 -#define DOM_VK_WIN_OEM_JUMP 0xEA -#define DOM_VK_WIN_OEM_PA1 0xEB -#define DOM_VK_WIN_OEM_PA2 0xEC -#define DOM_VK_WIN_OEM_PA3 0xED -#define DOM_VK_WIN_OEM_WSCTRL 0xEE -#define DOM_VK_WIN_OEM_CUSEL 0xEF -#define DOM_VK_WIN_OEM_ATTN 0xF0 -#define DOM_VK_WIN_OEM_FINISH 0xF1 -#define DOM_VK_WIN_OEM_COPY 0xF2 -#define DOM_VK_WIN_OEM_AUTO 0xF3 -#define DOM_VK_WIN_OEM_ENLW 0xF4 -#define DOM_VK_WIN_OEM_BACKTAB 0xF5 -#define DOM_VK_ATTN 0xF6 -#define DOM_VK_CRSEL 0xF7 -#define DOM_VK_EXSEL 0xF8 -#define DOM_VK_EREOF 0xF9 -#define DOM_VK_PLAY 0xFA -#define DOM_VK_ZOOM 0xFB -#define DOM_VK_PA1 0xFD -#define DOM_VK_WIN_OEM_CLEAR 0xFE - -int dom2godot_keycode(int dom_keycode) { - - if (DOM_VK_0 <= dom_keycode && dom_keycode <= DOM_VK_Z) { - // ASCII intersection - return dom_keycode; - } - - if (DOM_VK_NUMPAD0 <= dom_keycode && dom_keycode <= DOM_VK_NUMPAD9) { - // Numpad numbers - return KEY_KP_0 + (dom_keycode - DOM_VK_NUMPAD0); +// See https://w3c.github.io/uievents-code/#code-value-tables +int dom_code2godot_scancode(EM_UTF8 const p_code[32], EM_UTF8 const p_key[32], bool p_physical) { +#define DOM2GODOT(p_str, p_godot_code) \ + if (memcmp((const void *)p_str, (void *)p_code, strlen(p_str) + 1) == 0) { \ + return KEY_##p_godot_code; \ } - if (DOM_VK_F1 <= dom_keycode && dom_keycode <= DOM_VK_F16) { - // F1-F16 - return KEY_F1 + (dom_keycode - DOM_VK_F1); + // Numpad section. + DOM2GODOT("NumLock", NUMLOCK); + DOM2GODOT("Numpad0", KP_0); + DOM2GODOT("Numpad1", KP_1); + DOM2GODOT("Numpad2", KP_2); + DOM2GODOT("Numpad3", KP_3); + DOM2GODOT("Numpad4", KP_4); + DOM2GODOT("Numpad5", KP_5); + DOM2GODOT("Numpad6", KP_6); + DOM2GODOT("Numpad7", KP_7); + DOM2GODOT("Numpad8", KP_8); + DOM2GODOT("Numpad9", KP_9); + DOM2GODOT("NumpadAdd", KP_ADD); + DOM2GODOT("NumpadBackspace", BACKSPACE); + DOM2GODOT("NumpadClear", CLEAR); + DOM2GODOT("NumpadClearEntry", CLEAR); + //DOM2GODOT("NumpadComma", UNKNOWN); + DOM2GODOT("NumpadDecimal", KP_PERIOD); + DOM2GODOT("NumpadDivide", KP_DIVIDE); + DOM2GODOT("NumpadEnter", KP_ENTER); + DOM2GODOT("NumpadEqual", EQUAL); + //DOM2GODOT("NumpadHash", UNKNOWN); + //DOM2GODOT("NumpadMemoryAdd", UNKNOWN); + //DOM2GODOT("NumpadMemoryClear", UNKNOWN); + //DOM2GODOT("NumpadMemoryRecall", UNKNOWN); + //DOM2GODOT("NumpadMemoryStore", UNKNOWN); + //DOM2GODOT("NumpadMemorySubtract", UNKNOWN); + DOM2GODOT("NumpadMultiply", KP_MULTIPLY); + DOM2GODOT("NumpadParenLeft", PARENLEFT); + DOM2GODOT("NumpadParenRight", PARENRIGHT); + DOM2GODOT("NumpadStar", KP_MULTIPLY); // or ASTERISK ? + DOM2GODOT("NumpadSubtract", KP_SUBTRACT); + + // Printable ASCII. + if (!p_physical) { + uint8_t b0 = (uint8_t)p_key[0]; + uint8_t b1 = (uint8_t)p_key[1]; + uint8_t b2 = (uint8_t)p_key[2]; + if (b1 == 0 && b0 > 0x1F && b0 < 0x7F) { // ASCII. + if (b0 > 0x60 && b0 < 0x7B) { // Lowercase ASCII. + b0 -= 32; + } + return b0; + } + +#define _U_2BYTES_MASK 0xE0 +#define _U_2BYTES 0xC0 + // Latin-1 codes. + if (b2 == 0 && (b0 & _U_2BYTES_MASK) == _U_2BYTES) { // 2-bytes utf8, only known latin. + uint32_t key = ((b0 & ~_U_2BYTES_MASK) << 6) | (b1 & 0x3F); + if (key >= 0xA0 && key <= 0xDF) { + return key; + } + if (key >= 0xE0 && key <= 0xFF) { // Lowercase known latin. + key -= 0x20; + return key; + } + } +#undef _U_2BYTES_MASK +#undef _U_2BYTES } - switch (dom_keycode) { - //case DOM_VK_CANCEL: return KEY_UNKNOWN; - case DOM_VK_HELP: return KEY_HELP; - case DOM_VK_BACK_SPACE: return KEY_BACKSPACE; - case DOM_VK_TAB: return KEY_TAB; - - case DOM_VK_CLEAR: - case DOM_VK_WIN_OEM_CLEAR: // OEM duplicate - return KEY_CLEAR; - - case DOM_VK_RETURN: - case DOM_VK_ENTER: // unused according to MDN - return KEY_ENTER; - - case DOM_VK_SHIFT: return KEY_SHIFT; - case DOM_VK_CONTROL: return KEY_CONTROL; - - case DOM_VK_ALT: - case DOM_VK_ALTGR: - return KEY_ALT; - - case DOM_VK_PAUSE: return KEY_PAUSE; - case DOM_VK_CAPS_LOCK: - return KEY_CAPSLOCK; - - /* - case DOM_VK_KANA: return KEY_UNKNOWN; - case DOM_VK_HANGUL: return KEY_UNKNOWN; - case DOM_VK_EISU: return KEY_UNKNOWN; - case DOM_VK_JUNJA: return KEY_UNKNOWN; - case DOM_VK_FINAL: return KEY_UNKNOWN; - case DOM_VK_HANJA: return KEY_UNKNOWN; - case DOM_VK_KANJI: return KEY_UNKNOWN; - */ - - case DOM_VK_ESCAPE: - return KEY_ESCAPE; - /* - case DOM_VK_CONVERT: return KEY_UNKNOWN; - case DOM_VK_NONCONVERT: return KEY_UNKNOWN; - case DOM_VK_ACCEPT: return KEY_UNKNOWN; - case DOM_VK_MODECHANGE: return KEY_UNKNOWN; - */ - - case DOM_VK_SPACE: return KEY_SPACE; - case DOM_VK_PAGE_UP: return KEY_PAGEUP; - case DOM_VK_PAGE_DOWN: return KEY_PAGEDOWN; - case DOM_VK_END: return KEY_END; - case DOM_VK_HOME: return KEY_HOME; - case DOM_VK_LEFT: return KEY_LEFT; - case DOM_VK_UP: return KEY_UP; - case DOM_VK_RIGHT: return KEY_RIGHT; - case DOM_VK_DOWN: - return KEY_DOWN; - - //case DOM_VK_SELECT: return KEY_UNKNOWN; - - case DOM_VK_PRINTSCREEN: - case DOM_VK_PRINT: - return KEY_PRINT; - - //case DOM_VK_EXECUTE: return KEY_UNKNOWN; - case DOM_VK_INSERT: return KEY_INSERT; - case DOM_VK_DELETE: return KEY_DELETE; - - case DOM_VK_META: - case DOM_VK_WIN: - return KEY_META; - - case DOM_VK_CONTEXT_MENU: return KEY_MENU; - case DOM_VK_SLEEP: - return KEY_STANDBY; - - // Numpad keys - case DOM_VK_MULTIPLY: return KEY_KP_MULTIPLY; - case DOM_VK_ADD: return KEY_KP_ADD; - case DOM_VK_SEPARATOR: - return KEY_KP_PERIOD; // Good enough? - case DOM_VK_SUBTRACT: return KEY_KP_SUBTRACT; - case DOM_VK_DECIMAL: return KEY_KP_PERIOD; - case DOM_VK_DIVIDE: - return KEY_KP_DIVIDE; - - /* - case DOM_VK_F17: return KEY_UNKNOWN; - case DOM_VK_F18: return KEY_UNKNOWN; - case DOM_VK_F19: return KEY_UNKNOWN; - case DOM_VK_F20: return KEY_UNKNOWN; - case DOM_VK_F21: return KEY_UNKNOWN; - case DOM_VK_F22: return KEY_UNKNOWN; - case DOM_VK_F23: return KEY_UNKNOWN; - case DOM_VK_F24: return KEY_UNKNOWN; - */ - - case DOM_VK_NUM_LOCK: return KEY_NUMLOCK; - case DOM_VK_SCROLL_LOCK: - return KEY_SCROLLLOCK; - - /* - case DOM_VK_WIN_OEM_FJ_JISHO: return KEY_UNKNOWN; - case DOM_VK_WIN_OEM_FJ_MASSHOU: return KEY_UNKNOWN; - case DOM_VK_WIN_OEM_FJ_TOUROKU: return KEY_UNKNOWN; - case DOM_VK_WIN_OEM_FJ_LOYA: return KEY_UNKNOWN; - case DOM_VK_WIN_OEM_FJ_ROYA: return KEY_UNKNOWN; - */ - - case DOM_VK_CIRCUMFLEX: return KEY_ASCIICIRCUM; - case DOM_VK_EXCLAMATION: return KEY_EXCLAM; - case DOM_VK_DOUBLE_QUOTE: return KEY_QUOTEDBL; - case DOM_VK_HASH: return KEY_NUMBERSIGN; - case DOM_VK_DOLLAR: return KEY_DOLLAR; - case DOM_VK_PERCENT: return KEY_PERCENT; - case DOM_VK_AMPERSAND: return KEY_AMPERSAND; - case DOM_VK_UNDERSCORE: return KEY_UNDERSCORE; - case DOM_VK_OPEN_PAREN: return KEY_PARENLEFT; - case DOM_VK_CLOSE_PAREN: return KEY_PARENRIGHT; - case DOM_VK_ASTERISK: return KEY_ASTERISK; - case DOM_VK_PLUS: return KEY_PLUS; - case DOM_VK_PIPE: return KEY_BAR; - case DOM_VK_HYPHEN_MINUS: return KEY_MINUS; - case DOM_VK_OPEN_CURLY_BRACKET: return KEY_BRACELEFT; - case DOM_VK_CLOSE_CURLY_BRACKET: return KEY_BRACERIGHT; - case DOM_VK_TILDE: return KEY_ASCIITILDE; - - case DOM_VK_VOLUME_MUTE: return KEY_VOLUMEMUTE; - case DOM_VK_VOLUME_DOWN: return KEY_VOLUMEDOWN; - case DOM_VK_VOLUME_UP: return KEY_VOLUMEUP; - - case DOM_VK_COMMA: return KEY_COMMA; - case DOM_VK_PERIOD: return KEY_PERIOD; - case DOM_VK_SLASH: return KEY_SLASH; - case DOM_VK_BACK_QUOTE: return KEY_QUOTELEFT; - case DOM_VK_OPEN_BRACKET: return KEY_BRACKETLEFT; - case DOM_VK_BACK_SLASH: return KEY_BACKSLASH; - case DOM_VK_CLOSE_BRACKET: return KEY_BRACKETRIGHT; - case DOM_VK_QUOTE: - return KEY_APOSTROPHE; - - // The rest is OEM/unusual. - - default: return KEY_UNKNOWN; - }; + // Alphanumeric section. + DOM2GODOT("Backquote", QUOTELEFT); + DOM2GODOT("Backslash", BACKSLASH); + DOM2GODOT("BracketLeft", BRACKETLEFT); + DOM2GODOT("BracketRight", BRACKETRIGHT); + DOM2GODOT("Comma", COMMA); + DOM2GODOT("Digit0", 0); + DOM2GODOT("Digit1", 1); + DOM2GODOT("Digit2", 2); + DOM2GODOT("Digit3", 3); + DOM2GODOT("Digit4", 4); + DOM2GODOT("Digit5", 5); + DOM2GODOT("Digit6", 6); + DOM2GODOT("Digit7", 7); + DOM2GODOT("Digit8", 8); + DOM2GODOT("Digit9", 9); + DOM2GODOT("Equal", EQUAL); + DOM2GODOT("IntlBackslash", BACKSLASH); + //DOM2GODOT("IntlRo", UNKNOWN); + DOM2GODOT("IntlYen", YEN); + + DOM2GODOT("KeyA", A); + DOM2GODOT("KeyB", B); + DOM2GODOT("KeyC", C); + DOM2GODOT("KeyD", D); + DOM2GODOT("KeyE", E); + DOM2GODOT("KeyF", F); + DOM2GODOT("KeyG", G); + DOM2GODOT("KeyH", H); + DOM2GODOT("KeyI", I); + DOM2GODOT("KeyJ", J); + DOM2GODOT("KeyK", K); + DOM2GODOT("KeyL", L); + DOM2GODOT("KeyM", M); + DOM2GODOT("KeyN", N); + DOM2GODOT("KeyO", O); + DOM2GODOT("KeyP", P); + DOM2GODOT("KeyQ", Q); + DOM2GODOT("KeyR", R); + DOM2GODOT("KeyS", S); + DOM2GODOT("KeyT", T); + DOM2GODOT("KeyU", U); + DOM2GODOT("KeyV", V); + DOM2GODOT("KeyW", W); + DOM2GODOT("KeyX", X); + DOM2GODOT("KeyY", Y); + DOM2GODOT("KeyZ", Z); + + DOM2GODOT("Minus", MINUS); + DOM2GODOT("Period", PERIOD); + DOM2GODOT("Quote", APOSTROPHE); + DOM2GODOT("Semicolon", SEMICOLON); + DOM2GODOT("Slash", SLASH); + + // Functional keys in the Alphanumeric section. + DOM2GODOT("AltLeft", ALT); + DOM2GODOT("AltRight", ALT); + DOM2GODOT("Backspace", BACKSPACE); + DOM2GODOT("CapsLock", CAPSLOCK); + DOM2GODOT("ContextMenu", MENU); + DOM2GODOT("ControlLeft", CONTROL); + DOM2GODOT("ControlRight", CONTROL); + DOM2GODOT("Enter", ENTER); + DOM2GODOT("MetaLeft", SUPER_L); + DOM2GODOT("MetaRight", SUPER_R); + DOM2GODOT("ShiftLeft", SHIFT); + DOM2GODOT("ShiftRight", SHIFT); + DOM2GODOT("Space", SPACE); + DOM2GODOT("Tab", TAB); + + // ControlPad section. + DOM2GODOT("Delete", DELETE); + DOM2GODOT("End", END); + DOM2GODOT("Help", HELP); + DOM2GODOT("Home", HOME); + DOM2GODOT("Insert", INSERT); + DOM2GODOT("PageDown", PAGEDOWN); + DOM2GODOT("PageUp", PAGEUP); + + // ArrowPad section. + DOM2GODOT("ArrowDown", DOWN); + DOM2GODOT("ArrowLeft", LEFT); + DOM2GODOT("ArrowRight", RIGHT); + DOM2GODOT("ArrowUp", UP); + + // Function section. + DOM2GODOT("Escape", ESCAPE); + DOM2GODOT("F1", F1); + DOM2GODOT("F2", F2); + DOM2GODOT("F3", F3); + DOM2GODOT("F4", F4); + DOM2GODOT("F5", F5); + DOM2GODOT("F6", F6); + DOM2GODOT("F7", F7); + DOM2GODOT("F8", F8); + DOM2GODOT("F9", F9); + DOM2GODOT("F10", F10); + DOM2GODOT("F11", F11); + DOM2GODOT("F12", F12); + //DOM2GODOT("Fn", UNKNOWN); // never actually fired, but included in the standard draft. + //DOM2GODOT("FnLock", UNKNOWN); + DOM2GODOT("PrintScreen", PRINT); + DOM2GODOT("ScrollLock", SCROLLLOCK); + DOM2GODOT("Pause", PAUSE); + + // Media keys section. + DOM2GODOT("BrowserBack", BACK); + DOM2GODOT("BrowserFavorites", FAVORITES); + DOM2GODOT("BrowserForward", FORWARD); + DOM2GODOT("BrowserHome", OPENURL); + DOM2GODOT("BrowserRefresh", REFRESH); + DOM2GODOT("BrowserSearch", SEARCH); + DOM2GODOT("BrowserStop", STOP); + //DOM2GODOT("Eject", UNKNOWN); + DOM2GODOT("LaunchApp1", LAUNCH0); + DOM2GODOT("LaunchApp2", LAUNCH1); + DOM2GODOT("LaunchMail", LAUNCHMAIL); + DOM2GODOT("MediaPlayPause", MEDIAPLAY); + DOM2GODOT("MediaSelect", LAUNCHMEDIA); + DOM2GODOT("MediaStop", MEDIASTOP); + DOM2GODOT("MediaTrackNext", MEDIANEXT); + DOM2GODOT("MediaTrackPrevious", MEDIAPREVIOUS); + //DOM2GODOT("Power", UNKNOWN); + //DOM2GODOT("Sleep", UNKNOWN); + DOM2GODOT("AudioVolumeDown", VOLUMEDOWN); + DOM2GODOT("AudioVolumeMute", VOLUMEMUTE); + DOM2GODOT("AudioVolumeUp", VOLUMEUP); + //DOM2GODOT("WakeUp", UNKNOWN); + return KEY_UNKNOWN; +#undef DOM2GODOT } diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py index a55c9d3f48..8b8c492e22 100644 --- a/platform/javascript/emscripten_helpers.py +++ b/platform/javascript/emscripten_helpers.py @@ -1,28 +1,11 @@ import os - -def parse_config(): - em_config_file = os.getenv("EM_CONFIG") or os.path.expanduser("~/.emscripten") - if not os.path.exists(em_config_file): - raise RuntimeError("Emscripten configuration file '%s' does not exist" % em_config_file) - - normalized = {} - em_config = {} - with open(em_config_file) as f: - try: - # Emscripten configuration file is a Python file with simple assignments. - exec(f.read(), em_config) - except StandardError as e: - raise RuntimeError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e)) - normalized["EMCC_ROOT"] = em_config.get("EMSCRIPTEN_ROOT") - normalized["NODE_JS"] = em_config.get("NODE_JS") - normalized["CLOSURE_BIN"] = os.path.join(normalized["EMCC_ROOT"], "node_modules", ".bin", "google-closure-compiler") - return normalized +from SCons.Util import WhereIs def run_closure_compiler(target, source, env, for_signature): - cfg = parse_config() - cmd = [cfg["NODE_JS"], cfg["CLOSURE_BIN"]] + closure_bin = os.path.join(os.path.dirname(WhereIs("emcc")), "node_modules", ".bin", "google-closure-compiler") + cmd = [WhereIs("node"), closure_bin] cmd.extend(["--compilation_level", "ADVANCED_OPTIMIZATIONS"]) for f in env["JSEXTERNS"]: cmd.extend(["--externs", f.get_abspath()]) @@ -36,3 +19,15 @@ def create_engine_file(env, target, source, externs): if env["use_closure_compiler"]: return env.BuildJS(target, source, JSEXTERNS=externs) return env.Textfile(target, [env.File(s) for s in source]) + + +def add_js_libraries(env, libraries): + env.Append(JS_LIBS=env.File(libraries)) + + +def add_js_pre(env, js_pre): + env.Append(JS_PRE=env.File(js_pre)) + + +def add_js_externs(env, externs): + env.Append(JS_EXTERNS=env.File(externs)) diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js deleted file mode 100644 index 6d7509377f..0000000000 --- a/platform/javascript/engine/engine.js +++ /dev/null @@ -1,184 +0,0 @@ -Function('return this')()['Engine'] = (function() { - - var unloadAfterInit = true; - var canvas = null; - var resizeCanvasOnStart = false; - var customLocale = 'en_US'; - var wasmExt = '.wasm'; - - var preloader = new Preloader(); - var loader = new Loader(); - var rtenv = null; - - var executableName = ''; - 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() {}; - - Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { - if (initPromise) { - return initPromise; - } - if (!loadPromise) { - 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; - initPromise = loader.init(loadPromise, loadPath, config).then(function() { - return new Promise(function(resolve, reject) { - rtenv = loader.env; - if (unloadAfterInit) { - loadPromise = null; - } - resolve(); - }); - }); - 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 new Promise(function(resolve, reject) { - return me.init().then(function() { - if (!(canvas instanceof HTMLCanvasElement)) { - canvas = Utils.findCanvas(); - } - rtenv['locale'] = customLocale; - rtenv['canvas'] = canvas; - rtenv['thisProgram'] = executableName; - rtenv['resizeCanvasOnStart'] = resizeCanvasOnStart; - loader.start(preloader.preloadedFiles, args).then(function() { - loader = null; - initPromise = null; - resolve(); - }); - }); - }); - }; - - Engine.prototype.startGame = function(execName, mainPack) { - // Start and init with execName as loadPath if not inited. - executableName = execName; - var me = this; - return Promise.all([ - this.init(execName), - this.preloadFile(mainPack, mainPack) - ]).then(function() { - return me.start('--main-pack', mainPack); - }); - }; - - 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) { - canvas = canvasElem; - }; - - Engine.prototype.setCanvasResizedOnStart = function(enabled) { - resizeCanvasOnStart = enabled; - }; - - Engine.prototype.setLocale = function(locale) { - customLocale = locale; - }; - - Engine.prototype.setExecutableName = function(newName) { - 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 (rtenv) - 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 (rtenv) - rtenv.printErr = printErr; - stderr = printErr; - }; - - // 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 - return Engine; -})(); diff --git a/platform/javascript/engine/loader.js b/platform/javascript/engine/loader.js deleted file mode 100644 index d27fbf612e..0000000000 --- a/platform/javascript/engine/loader.js +++ /dev/null @@ -1,33 +0,0 @@ -var Loader = /** @constructor */ function() { - - this.env = null; - - this.init = function(loadPromise, basePath, config) { - var me = this; - return new Promise(function(resolve, reject) { - var cfg = config || {}; - cfg['locateFile'] = Utils.createLocateRewrite(basePath); - cfg['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); - loadPromise = null; - Godot(cfg).then(function(module) { - me.env = module; - resolve(); - }); - }); - } - - this.start = function(preloadedFiles, args) { - var me = this; - return new Promise(function(resolve, reject) { - if (!me.env) { - reject(new Error('The engine must be initialized before it can be started')); - } - preloadedFiles.forEach(function(file) { - Utils.copyToFS(me.env['FS'], file.path, file.buffer); - }); - preloadedFiles.length = 0; // Clear memory - me.env['callMain'](args); - resolve(); - }); - } -}; diff --git a/platform/javascript/engine/preloader.js b/platform/javascript/engine/preloader.js deleted file mode 100644 index 17918eae38..0000000000 --- a/platform/javascript/engine/preloader.js +++ /dev/null @@ -1,139 +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 fdff90a923..0000000000 --- a/platform/javascript/engine/utils.js +++ /dev/null @@ -1,69 +0,0 @@ -var Utils = { - - createLocateRewrite: function(execName) { - function rw(path) { - if (path.endsWith('.worker.js')) { - return execName + '.worker.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; - }, - - copyToFS: function(fs, path, buffer) { - var p = path.lastIndexOf("/"); - var dir = "/"; - if (p > 0) { - dir = path.slice(0, path.lastIndexOf("/")); - } - try { - fs.stat(dir); - } catch (e) { - if (e.errno !== 44) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h - throw e; - } - fs['mkdirTree'](dir); - } - // With memory growth, canOwn should be false. - fs['writeFile'](path, new Uint8Array(buffer), {'flags': 'wx+'}); - }, - - 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/export/export.cpp b/platform/javascript/export/export.cpp index 36076a2af9..48ccc1f87a 100644 --- a/platform/javascript/export/export.cpp +++ b/platform/javascript/export/export.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -28,6 +28,7 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#include "core/io/json.h" #include "core/io/tcp_server.h" #include "core/io/zip_io.h" #include "editor/editor_export.h" @@ -36,17 +37,13 @@ #include "platform/javascript/logo.gen.h" #include "platform/javascript/run_icon.gen.h" -#define EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE "webassembly_release.zip" -#define EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG "webassembly_debug.zip" - class EditorHTTPServer : public Reference { - private: Ref<TCP_Server> server; Ref<StreamPeerTCP> connection; - uint64_t time; + uint64_t time = 0; uint8_t req_buf[4096]; - int req_pos; + int req_pos = 0; void _clear_client() { connection = Ref<StreamPeerTCP>(); @@ -85,33 +82,44 @@ public: // Wrong protocol ERR_FAIL_COND_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", "Invalid method or HTTP version."); - String filepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("tmp_js_export"); + const String cache_path = EditorSettings::get_singleton()->get_cache_dir(); const String basereq = "/tmp_js_export"; - String ctype = ""; + String filepath; + String ctype; if (req[1] == basereq + ".html") { - filepath += ".html"; + filepath = cache_path.plus_file(req[1].get_file()); ctype = "text/html"; } else if (req[1] == basereq + ".js") { - filepath += ".js"; + filepath = cache_path.plus_file(req[1].get_file()); + ctype = "application/javascript"; + } else if (req[1] == basereq + ".audio.worklet.js") { + filepath = cache_path.plus_file(req[1].get_file()); ctype = "application/javascript"; } else if (req[1] == basereq + ".worker.js") { - filepath += ".worker.js"; + filepath = cache_path.plus_file(req[1].get_file()); ctype = "application/javascript"; } else if (req[1] == basereq + ".pck") { - filepath += ".pck"; + filepath = cache_path.plus_file(req[1].get_file()); ctype = "application/octet-stream"; } else if (req[1] == basereq + ".png" || req[1] == "/favicon.png") { // Also allow serving the generated favicon for a smoother loading experience. if (req[1] == "/favicon.png") { filepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("favicon.png"); } else { - filepath += ".png"; + filepath = basereq + ".png"; } ctype = "image/png"; + } else if (req[1] == basereq + ".side.wasm") { + filepath = cache_path.plus_file(req[1].get_file()); + ctype = "application/wasm"; } else if (req[1] == basereq + ".wasm") { - filepath += ".wasm"; + filepath = cache_path.plus_file(req[1].get_file()); ctype = "application/wasm"; - } else { + } else if (req[1].ends_with(".wasm")) { + filepath = cache_path.plus_file(req[1].get_file()); // TODO dangerous? + ctype = "application/wasm"; + } + if (filepath.is_empty() || !FileAccess::exists(filepath)) { String s = "HTTP/1.1 404 Not Found\r\n"; s += "Connection: Close\r\n"; s += "\r\n"; @@ -124,6 +132,9 @@ public: String s = "HTTP/1.1 200 OK\r\n"; s += "Connection: Close\r\n"; s += "Content-Type: " + ctype + "\r\n"; + s += "Access-Control-Allow-Origin: *\r\n"; + s += "Cross-Origin-Opener-Policy: same-origin\r\n"; + s += "Cross-Origin-Embedder-Policy: require-corp\r\n"; s += "\r\n"; CharString cs = s.utf8(); Error err = connection->put_data((const uint8_t *)cs.get_data(), cs.size() - 1); @@ -148,11 +159,13 @@ public: } void poll() { - if (!server->is_listening()) + if (!server->is_listening()) { return; + } if (connection.is_null()) { - if (!server->is_connection_available()) + if (!server->is_connection_available()) { return; + } connection = server->take_connection(); time = OS::get_singleton()->get_ticks_usec(); } @@ -160,11 +173,11 @@ public: _clear_client(); return; } - if (connection->get_status() != StreamPeerTCP::STATUS_CONNECTED) + if (connection->get_status() != StreamPeerTCP::STATUS_CONNECTED) { return; + } while (true) { - char *r = (char *)req_buf; int l = req_pos - 1; if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { @@ -190,71 +203,105 @@ public: }; class EditorExportPlatformJavaScript : public EditorExportPlatform { - GDCLASS(EditorExportPlatformJavaScript, EditorExportPlatform); Ref<ImageTexture> logo; Ref<ImageTexture> run_icon; Ref<ImageTexture> stop_icon; - int menu_options; + int menu_options = 0; - void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug); - -private: Ref<EditorHTTPServer> server; - bool server_quit; + bool server_quit = false; Mutex server_lock; - Thread *server_thread; + Thread *server_thread = nullptr; + + enum ExportMode { + EXPORT_MODE_NORMAL = 0, + EXPORT_MODE_THREADS = 1, + EXPORT_MODE_GDNATIVE = 2, + }; + + String _get_template_name(ExportMode p_mode, bool p_debug) const { + String name = "webassembly"; + switch (p_mode) { + case EXPORT_MODE_THREADS: + name += "_threads"; + break; + case EXPORT_MODE_GDNATIVE: + name += "_gdnative"; + break; + default: + break; + } + if (p_debug) { + name += "_debug.zip"; + } else { + name += "_release.zip"; + } + return name; + } + + void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects); static void _server_thread_poll(void *data); public: - virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features); + virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) override; - virtual void get_export_options(List<ExportOption> *r_options); + virtual void get_export_options(List<ExportOption> *r_options) override; - virtual String get_name() const; - virtual String get_os_name() const; - virtual Ref<Texture2D> get_logo() const; + virtual String get_name() const override; + virtual String get_os_name() const override; + virtual Ref<Texture2D> get_logo() const override; - virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const; - virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const; - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0); + virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; - virtual bool poll_export(); - virtual int get_options_count() const; - virtual String get_option_label(int p_index) const { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); } - virtual String get_option_tooltip(int p_index) const { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); } - virtual Ref<ImageTexture> get_option_icon(int p_index) const; - virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags); - virtual Ref<Texture2D> get_run_icon() const; - - virtual void get_platform_features(List<String> *r_features) { + virtual bool poll_export() override; + virtual int get_options_count() const override; + virtual String get_option_label(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); } + virtual String get_option_tooltip(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); } + virtual Ref<ImageTexture> get_option_icon(int p_index) const override; + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) override; + virtual Ref<Texture2D> get_run_icon() const override; + virtual void get_platform_features(List<String> *r_features) override { r_features->push_back("web"); r_features->push_back(get_os_name()); } - virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) { + virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override { } + String get_debug_protocol() const override { return "ws://"; } + EditorExportPlatformJavaScript(); ~EditorExportPlatformJavaScript(); }; -void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug) { - +void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects) { String str_template = String::utf8(reinterpret_cast<const char *>(p_html.ptr()), p_html.size()); String str_export; Vector<String> lines = str_template.split("\n"); + Vector<String> flags; + String flags_json; + gen_export_flags(flags, p_flags); + flags_json = JSON::print(flags); + String libs; + for (int i = 0; i < p_shared_objects.size(); i++) { + libs += "\"" + p_shared_objects[i].path.get_file() + "\","; + } for (int i = 0; i < lines.size(); i++) { - String current_line = lines[i]; current_line = current_line.replace("$GODOT_BASENAME", p_name); current_line = current_line.replace("$GODOT_PROJECT_NAME", ProjectSettings::get_singleton()->get_setting("application/config/name")); current_line = current_line.replace("$GODOT_HEAD_INCLUDE", p_preset->get("html/head_include")); + current_line = current_line.replace("$GODOT_FULL_WINDOW", p_preset->get("html/full_window_size") ? "true" : "false"); + current_line = current_line.replace("$GODOT_GDNATIVE_LIBS", libs); current_line = current_line.replace("$GODOT_DEBUG_ENABLED", p_debug ? "true" : "false"); + current_line = current_line.replace("$GODOT_ARGS", flags_json); str_export += current_line + "\n"; } @@ -266,7 +313,6 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re } void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) { - if (p_preset->get("vram_texture_compression/for_desktop")) { r_features->push_back("s3tc"); } @@ -280,42 +326,47 @@ void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportP r_features->push_back("etc2"); } } + ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type"); + if (mode == EXPORT_MODE_THREADS) { + r_features->push_back("threads"); + } else if (mode == EXPORT_MODE_GDNATIVE) { + r_features->push_back("wasm32"); + } } void EditorExportPlatformJavaScript::get_export_options(List<ExportOption> *r_options) { + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "variant/export_type", PROPERTY_HINT_ENUM, "Regular,Threads,GDNative"), 0)); // Export type. r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/custom_html_shell", PROPERTY_HINT_FILE, "*.html"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/head_include", PROPERTY_HINT_MULTILINE_TEXT), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/full_window_size"), true)); } String EditorExportPlatformJavaScript::get_name() const { - return "HTML5"; } String EditorExportPlatformJavaScript::get_os_name() const { - return "HTML5"; } Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const { - return logo; } bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { - String err; bool valid = false; + ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type"); // Look for export templates (first official, and if defined custom templates). - - bool dvalid = exists_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG, &err); - bool rvalid = exists_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE, &err); + bool dvalid = exists_export_template(_get_template_name(mode, true), &err); + bool rvalid = exists_export_template(_get_template_name(mode, false), &err); if (p_preset->get("custom_template/debug") != "") { dvalid = FileAccess::exists(p_preset->get("custom_template/debug")); @@ -343,14 +394,14 @@ bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p } } - if (!err.empty()) + if (!err.is_empty()) { r_error = err; + } return valid; } List<String> EditorExportPlatformJavaScript::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const { - List<String> list; list.push_back("html"); return list; @@ -368,11 +419,8 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese template_path = template_path.strip_edges(); if (template_path == String()) { - - if (p_debug) - template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG); - else - template_path = find_export_template(EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE); + ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type"); + template_path = find_export_template(_get_template_name(mode, p_debug)); } if (!DirAccess::exists(p_path.get_base_dir())) { @@ -384,19 +432,30 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese return ERR_FILE_NOT_FOUND; } + Vector<SharedObject> shared_objects; String pck_path = p_path.get_basename() + ".pck"; - Error error = save_pack(p_preset, pck_path); + Error error = save_pack(p_preset, pck_path, &shared_objects); if (error != OK) { EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + pck_path); return error; } + DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + for (int i = 0; i < shared_objects.size(); i++) { + String dst = p_path.get_base_dir().plus_file(shared_objects[i].path.get_file()); + error = da->copy(shared_objects[i].path, dst); + if (error != OK) { + EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + shared_objects[i].path.get_file()); + memdelete(da); + return error; + } + } + memdelete(da); FileAccess *src_f = nullptr; zlib_filefunc_def io = zipio_create_io_from_file(&src_f); unzFile pkg = unzOpen2(template_path.utf8().get_data(), &io); if (!pkg) { - EditorNode::get_singleton()->show_warning(TTR("Could not open template for export:") + "\n" + template_path); return ERR_FILE_NOT_FOUND; } @@ -426,22 +485,25 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese //write if (file == "godot.html") { - - if (!custom_html.empty()) { + if (!custom_html.is_empty()) { continue; } - _fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug); + _fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug, p_flags, shared_objects); file = p_path.get_file(); } else if (file == "godot.js") { - file = p_path.get_file().get_basename() + ".js"; - } else if (file == "godot.worker.js") { + } else if (file == "godot.worker.js") { file = p_path.get_file().get_basename() + ".worker.js"; - } else if (file == "godot.wasm") { + } else if (file == "godot.side.wasm") { + file = p_path.get_file().get_basename() + ".side.wasm"; + } else if (file == "godot.audio.worklet.js") { + file = p_path.get_file().get_basename() + ".audio.worklet.js"; + + } else if (file == "godot.wasm") { file = p_path.get_file().get_basename() + ".wasm"; } @@ -458,8 +520,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese } while (unzGoToNextFile(pkg) == UNZ_OK); unzClose(pkg); - if (!custom_html.empty()) { - + if (!custom_html.is_empty()) { FileAccess *f = FileAccess::open(custom_html, FileAccess::READ); if (!f) { EditorNode::get_singleton()->show_warning(TTR("Could not read custom HTML shell:") + "\n" + custom_html); @@ -469,7 +530,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese buf.resize(f->get_len()); f->get_buffer(buf.ptrw(), buf.size()); memdelete(f); - _fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug); + _fix_html(buf, p_preset, p_path.get_file().get_basename(), p_debug, p_flags, shared_objects); f = FileAccess::open(p_path, FileAccess::WRITE); if (!f) { @@ -482,7 +543,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese Ref<Image> splash; const String splash_path = String(GLOBAL_GET("application/boot_splash/image")).strip_edges(); - if (!splash_path.empty()) { + if (!splash_path.is_empty()) { splash.instance(); const Error err = splash->load(splash_path); if (err) { @@ -503,7 +564,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese // This way, the favicon can be displayed immediately when loading the page. Ref<Image> favicon; const String favicon_path = String(GLOBAL_GET("application/config/icon")).strip_edges(); - if (!favicon_path.empty()) { + if (!favicon_path.is_empty()) { favicon.instance(); const Error err = favicon->load(favicon_path); if (err) { @@ -523,11 +584,9 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese } bool EditorExportPlatformJavaScript::poll_export() { - Ref<EditorExportPreset> preset; for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) { - Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i); if (ep->is_runnable() && ep->get_platform() == this) { preset = ep; @@ -553,12 +612,10 @@ Ref<ImageTexture> EditorExportPlatformJavaScript::get_option_icon(int p_index) c } int EditorExportPlatformJavaScript::get_options_count() const { - return menu_options; } Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) { - if (p_option == 1) { MutexLock lock(server_lock); server->stop(); @@ -572,8 +629,10 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese DirAccess::remove_file_or_error(basepath + ".html"); DirAccess::remove_file_or_error(basepath + ".js"); DirAccess::remove_file_or_error(basepath + ".worker.js"); + DirAccess::remove_file_or_error(basepath + ".audio.worklet.js"); DirAccess::remove_file_or_error(basepath + ".pck"); DirAccess::remove_file_or_error(basepath + ".png"); + DirAccess::remove_file_or_error(basepath + ".side.wasm"); DirAccess::remove_file_or_error(basepath + ".wasm"); DirAccess::remove_file_or_error(EditorSettings::get_singleton()->get_cache_dir().plus_file("favicon.png")); return err; @@ -606,7 +665,6 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese } Ref<Texture2D> EditorExportPlatformJavaScript::get_run_icon() const { - return run_icon; } @@ -622,9 +680,7 @@ void EditorExportPlatformJavaScript::_server_thread_poll(void *data) { } EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() { - server.instance(); - server_quit = false; server_thread = Thread::create(_server_thread_poll, this); Ref<Image> img = memnew(Image(_javascript_logo)); @@ -636,12 +692,11 @@ EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() { run_icon->create_from_image(img); Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); - if (theme.is_valid()) + if (theme.is_valid()) { stop_icon = theme->get_icon("Stop", "EditorIcons"); - else + } else { stop_icon.instance(); - - menu_options = 0; + } } EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() { @@ -652,7 +707,6 @@ EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() { } void register_javascript_exporter() { - EDITOR_DEF("export/web/http_host", "localhost"); EDITOR_DEF("export/web/http_port", 8060); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "export/web/http_port", PROPERTY_HINT_RANGE, "1,65535,1")); diff --git a/platform/javascript/export/export.h b/platform/javascript/export/export.h index 30c5c855d1..e641339f55 100644 --- a/platform/javascript/export/export.h +++ b/platform/javascript/export/export.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/platform/javascript/godot_audio.h b/platform/javascript/godot_audio.h new file mode 100644 index 0000000000..54fc8fa3b5 --- /dev/null +++ b/platform/javascript/godot_audio.h @@ -0,0 +1,63 @@ +/*************************************************************************/ +/* godot_audio.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_AUDIO_H +#define GODOT_AUDIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "stddef.h" + +extern int godot_audio_is_available(); +extern int godot_audio_init(int p_mix_rate, int p_latency, void (*_state_cb)(int), void (*_latency_cb)(float)); +extern void godot_audio_resume(); + +extern int godot_audio_capture_start(); +extern void godot_audio_capture_stop(); + +// Worklet +typedef int32_t GodotAudioState[4]; +extern void godot_audio_worklet_create(int p_channels); +extern void godot_audio_worklet_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, GodotAudioState p_state); +extern int godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value); +extern int godot_audio_worklet_state_get(GodotAudioState p_state, int p_idx); +extern int godot_audio_worklet_state_wait(int32_t *p_state, int p_idx, int32_t p_expected, int p_timeout); + +// Script +extern int godot_audio_script_create(int p_buffer_size, int p_channels); +extern void godot_audio_script_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, void (*p_cb)()); + +#ifdef __cplusplus +} +#endif + +#endif /* GODOT_AUDIO_H */ diff --git a/platform/javascript/godot_js.h b/platform/javascript/godot_js.h new file mode 100644 index 0000000000..5b98253b08 --- /dev/null +++ b/platform/javascript/godot_js.h @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* godot_js.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef GODOT_JS_H +#define GODOT_JS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "stddef.h" + +// Config +extern void godot_js_config_locale_get(char *p_ptr, int p_ptr_max); +extern void godot_js_config_canvas_id_get(char *p_ptr, int p_ptr_max); +extern int godot_js_config_is_resize_on_start(); + +// OS +extern void godot_js_os_finish_async(void (*p_callback)()); +extern void godot_js_os_request_quit_cb(void (*p_callback)()); +extern int godot_js_os_fs_is_persistent(); +extern void godot_js_os_fs_sync(void (*p_callback)()); +extern int godot_js_os_execute(const char *p_json); +extern void godot_js_os_shell_open(const char *p_uri); + +// Display +extern double godot_js_display_pixel_ratio_get(); +extern void godot_js_display_alert(const char *p_text); +extern int godot_js_display_touchscreen_is_available(); +extern int godot_js_display_is_swap_ok_cancel(); + +// Display canvas +extern void godot_js_display_canvas_focus(); +extern int godot_js_display_canvas_is_focused(); +extern void godot_js_display_canvas_bounding_rect_position_get(int32_t *p_x, int32_t *p_y); + +// Display window +extern void godot_js_display_window_request_fullscreen(); +extern void godot_js_display_window_title_set(const char *p_text); +extern void godot_js_display_window_icon_set(const uint8_t *p_ptr, int p_len); + +// Display clipboard +extern int godot_js_display_clipboard_set(const char *p_text); +extern int godot_js_display_clipboard_get(void (*p_callback)(const char *p_text)); + +// Display cursor +extern void godot_js_display_cursor_set_shape(const char *p_cursor); +extern int godot_js_display_cursor_is_hidden(); +extern void godot_js_display_cursor_set_custom_shape(const char *p_shape, const uint8_t *p_ptr, int p_len, int p_hotspot_x, int p_hotspot_y); +extern void godot_js_display_cursor_set_visible(int p_visible); + +// Display listeners +extern void godot_js_display_notification_cb(void (*p_callback)(int p_notification), int p_enter, int p_exit, int p_in, int p_out); +extern void godot_js_display_paste_cb(void (*p_callback)(const char *p_text)); +extern void godot_js_display_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); +#ifdef __cplusplus +} +#endif + +#endif /* GODOT_JS_H */ diff --git a/platform/javascript/http_client.h.inc b/platform/javascript/http_client.h.inc index ac275aadbc..2ef1ad5b83 100644 --- a/platform/javascript/http_client.h.inc +++ b/platform/javascript/http_client.h.inc @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -33,21 +33,21 @@ Error prepare_request(Method p_method, const String &p_url, const Vector<String> &p_headers); int xhr_id; -int read_limit; -int response_read_offset; -Status status; +int read_limit = 4096; +int response_read_offset = 0; +Status status = STATUS_DISCONNECTED; String host; -int port; -bool use_tls; +int port = -1; +bool use_tls = false; String username; String password; -int polled_response_code; +int polled_response_code = 0; String polled_response_header; PackedByteArray polled_response; #ifdef DEBUG_ENABLED -bool has_polled; -uint64_t last_polling_frame; +bool has_polled = false; +uint64_t last_polling_frame = 0; #endif diff --git a/platform/javascript/http_client_javascript.cpp b/platform/javascript/http_client_javascript.cpp index 863c207896..c8c48dd582 100644 --- a/platform/javascript/http_client_javascript.cpp +++ b/platform/javascript/http_client_javascript.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -29,10 +29,10 @@ /*************************************************************************/ #include "core/io/http_client.h" + #include "http_request.h" Error HTTPClient::connect_to_host(const String &p_host, int p_port, bool p_ssl, bool p_verify_host) { - close(); if (p_ssl && !p_verify_host) { WARN_PRINT("Disabling HTTPClient's host verification is not supported for the HTML5 platform, host will be verified"); @@ -67,29 +67,26 @@ Error HTTPClient::connect_to_host(const String &p_host, int p_port, bool p_ssl, } void HTTPClient::set_connection(const Ref<StreamPeer> &p_connection) { - ERR_FAIL_MSG("Accessing an HTTPClient's StreamPeer is not supported for the HTML5 platform."); } Ref<StreamPeer> HTTPClient::get_connection() const { - ERR_FAIL_V_MSG(REF(), "Accessing an HTTPClient's StreamPeer is not supported for the HTML5 platform."); } Error HTTPClient::prepare_request(Method p_method, const String &p_url, const Vector<String> &p_headers) { - ERR_FAIL_INDEX_V(p_method, METHOD_MAX, ERR_INVALID_PARAMETER); ERR_FAIL_COND_V_MSG(p_method == METHOD_TRACE || p_method == METHOD_CONNECT, ERR_UNAVAILABLE, "HTTP methods TRACE and CONNECT are not supported for the HTML5 platform."); ERR_FAIL_COND_V(status != STATUS_CONNECTED, ERR_INVALID_PARAMETER); - ERR_FAIL_COND_V(host.empty(), ERR_UNCONFIGURED); + ERR_FAIL_COND_V(host.is_empty(), ERR_UNCONFIGURED); ERR_FAIL_COND_V(port < 0, ERR_UNCONFIGURED); ERR_FAIL_COND_V(!p_url.begins_with("/"), ERR_INVALID_PARAMETER); String url = (use_tls ? "https://" : "http://") + host + ":" + itos(port) + p_url; godot_xhr_reset(xhr_id); godot_xhr_open(xhr_id, _methods[p_method], url.utf8().get_data(), - username.empty() ? nullptr : username.utf8().get_data(), - password.empty() ? nullptr : password.utf8().get_data()); + username.is_empty() ? nullptr : username.utf8().get_data(), + password.is_empty() ? nullptr : password.utf8().get_data()); for (int i = 0; i < p_headers.size(); i++) { int header_separator = p_headers[i].find(": "); @@ -104,7 +101,6 @@ Error HTTPClient::prepare_request(Method p_method, const String &p_url, const Ve } Error HTTPClient::request_raw(Method p_method, const String &p_url, const Vector<String> &p_headers, const Vector<uint8_t> &p_body) { - Error err = prepare_request(p_method, p_url, p_headers); if (err != OK) return err; @@ -113,7 +109,6 @@ Error HTTPClient::request_raw(Method p_method, const String &p_url, const Vector } Error HTTPClient::request(Method p_method, const String &p_url, const Vector<String> &p_headers, const String &p_body) { - Error err = prepare_request(p_method, p_url, p_headers); if (err != OK) return err; @@ -122,7 +117,6 @@ Error HTTPClient::request(Method p_method, const String &p_url, const Vector<Str } void HTTPClient::close() { - host = ""; port = -1; use_tls = false; @@ -134,29 +128,24 @@ void HTTPClient::close() { } HTTPClient::Status HTTPClient::get_status() const { - return status; } bool HTTPClient::has_response() const { - - return !polled_response_header.empty(); + return !polled_response_header.is_empty(); } bool HTTPClient::is_response_chunked() const { - // TODO evaluate using moz-chunked-arraybuffer, fetch & ReadableStream return false; } int HTTPClient::get_response_code() const { - return polled_response_code; } Error HTTPClient::get_response_headers(List<String> *r_response) { - - if (polled_response_header.empty()) + if (polled_response_header.is_empty()) return ERR_INVALID_PARAMETER; Vector<String> header_lines = polled_response_header.split("\r\n", false); @@ -168,12 +157,10 @@ Error HTTPClient::get_response_headers(List<String> *r_response) { } int HTTPClient::get_response_body_length() const { - return polled_response.size(); } PackedByteArray HTTPClient::read_response_body_chunk() { - ERR_FAIL_COND_V(status != STATUS_BODY, PackedByteArray()); int to_read = MIN(read_limit, polled_response.size() - response_read_offset); @@ -192,17 +179,14 @@ PackedByteArray HTTPClient::read_response_body_chunk() { } void HTTPClient::set_blocking_mode(bool p_enable) { - ERR_FAIL_COND_MSG(p_enable, "HTTPClient blocking mode is not supported for the HTML5 platform."); } bool HTTPClient::is_blocking_mode_enabled() const { - return false; } void HTTPClient::set_read_chunk_size(int p_size) { - read_limit = p_size; } @@ -211,9 +195,7 @@ int HTTPClient::get_read_chunk_size() const { } Error HTTPClient::poll() { - switch (status) { - case STATUS_DISCONNECTED: return ERR_UNCONFIGURED; @@ -233,19 +215,18 @@ Error HTTPClient::poll() { return ERR_CONNECTION_ERROR; case STATUS_REQUESTING: { - #ifdef DEBUG_ENABLED if (!has_polled) { has_polled = true; } else { // forcing synchronous requests is not possible on the web - if (last_polling_frame == Engine::get_singleton()->get_idle_frames()) { + if (last_polling_frame == Engine::get_singleton()->get_process_frames()) { WARN_PRINT("HTTPClient polled multiple times in one frame, " "but request cannot progress more than once per " "frame on the HTML5 platform."); } } - last_polling_frame = Engine::get_singleton()->get_idle_frames(); + last_polling_frame = Engine::get_singleton()->get_process_frames(); #endif polled_response_code = godot_xhr_get_status(xhr_id); @@ -279,20 +260,9 @@ Error HTTPClient::poll() { } HTTPClient::HTTPClient() { - xhr_id = godot_xhr_new(); - read_limit = 4096; - status = STATUS_DISCONNECTED; - port = -1; - use_tls = false; - polled_response_code = 0; -#ifdef DEBUG_ENABLED - has_polled = false; - last_polling_frame = 0; -#endif } HTTPClient::~HTTPClient() { - godot_xhr_free(xhr_id); } diff --git a/platform/javascript/http_request.h b/platform/javascript/http_request.h index 54e98c1927..d32b2f265e 100644 --- a/platform/javascript/http_request.h +++ b/platform/javascript/http_request.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -47,7 +47,7 @@ typedef enum { extern int godot_xhr_new(); extern void godot_xhr_reset(int p_xhr_id); -extern bool godot_xhr_free(int p_xhr_id); +extern void godot_xhr_free(int p_xhr_id); extern int godot_xhr_open(int p_xhr_id, const char *p_method, const char *p_url, const char *p_user = nullptr, const char *p_password = nullptr); diff --git a/platform/javascript/javascript_eval.cpp b/platform/javascript/javascript_eval.cpp index db8050b90e..cb19dd20d4 100644 --- a/platform/javascript/javascript_eval.cpp +++ b/platform/javascript/javascript_eval.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -33,97 +33,30 @@ #include "api/javascript_eval.h" #include "emscripten.h" -extern "C" EMSCRIPTEN_KEEPALIVE uint8_t *resize_PackedByteArray_and_open_write(PackedByteArray *p_arr, VectorWriteProxy<uint8_t> *r_write, int p_len) { +extern "C" { +union js_eval_ret { + uint32_t b; + double d; + char *s; +}; - p_arr->resize(p_len); - *r_write = p_arr->write; - return p_arr->ptrw(); +extern int godot_js_eval(const char *p_js, int p_use_global_ctx, union js_eval_ret *p_union_ptr, void *p_byte_arr, void *p_byte_arr_write, void *(*p_callback)(void *p_ptr, void *p_ptr2, int p_len)); } -Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { - - union { - bool b; - double d; - char *s; - } js_data; +void *resize_PackedByteArray_and_open_write(void *p_arr, void *r_write, int p_len) { + PackedByteArray *arr = (PackedByteArray *)p_arr; + VectorWriteProxy<uint8_t> *write = (VectorWriteProxy<uint8_t> *)r_write; + arr->resize(p_len); + *write = arr->write; + return arr->ptrw(); +} +Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { + union js_eval_ret js_data; PackedByteArray arr; VectorWriteProxy<uint8_t> arr_write; - /* clang-format off */ - Variant::Type return_type = static_cast<Variant::Type>(EM_ASM_INT({ - - const CODE = $0; - const USE_GLOBAL_EXEC_CONTEXT = $1; - const PTR = $2; - const BYTEARRAY_PTR = $3; - const BYTEARRAY_WRITE_PTR = $4; - var eval_ret; - try { - if (USE_GLOBAL_EXEC_CONTEXT) { - // indirect eval call grants global execution context - var global_eval = eval; - eval_ret = global_eval(UTF8ToString(CODE)); - } else { - eval_ret = eval(UTF8ToString(CODE)); - } - } catch (e) { - err(e); - eval_ret = null; - } - - switch (typeof eval_ret) { - - case 'boolean': - setValue(PTR, eval_ret, 'i32'); - return 1; // BOOL - - case 'number': - setValue(PTR, eval_ret, 'double'); - return 3; // FLOAT - - case 'string': - var array_len = lengthBytesUTF8(eval_ret)+1; - var array_ptr = _malloc(array_len); - try { - if (array_ptr===0) { - throw new Error('String allocation failed (probably out of memory)'); - } - setValue(PTR, array_ptr , '*'); - stringToUTF8(eval_ret, array_ptr, array_len); - return 4; // STRING - } catch (e) { - if (array_ptr!==0) { - _free(array_ptr) - } - err(e); - // fall through - } - break; - - 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) { - var bytes_ptr = ccall('resize_PackedByteArray_and_open_write', 'number', ['number', 'number' ,'number'], [BYTEARRAY_PTR, BYTEARRAY_WRITE_PTR, eval_ret.length]); - HEAPU8.set(eval_ret, bytes_ptr); - return 20; // PACKED_BYTE_ARRAY - } - break; - } - return 0; // NIL - - }, p_code.utf8().get_data(), p_use_global_exec_context, &js_data, &arr, &arr_write)); - /* clang-format on */ + Variant::Type return_type = static_cast<Variant::Type>(godot_js_eval(p_code.utf8().get_data(), p_use_global_exec_context, &js_data, &arr, &arr_write, resize_PackedByteArray_and_open_write)); switch (return_type) { case Variant::BOOL: @@ -132,9 +65,7 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { return js_data.d; case Variant::STRING: { String str = String::utf8(js_data.s); - /* clang-format off */ - EM_ASM_({ _free($0); }, js_data.s); - /* clang-format on */ + free(js_data.s); // Must free the string allocated in JS. return str; } case Variant::PACKED_BYTE_ARRAY: diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 815bc7e456..0b8af70b13 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,42 +30,68 @@ #include "core/io/resource_loader.h" #include "main/main.h" -#include "os_javascript.h" +#include "platform/javascript/display_server_javascript.h" +#include "platform/javascript/os_javascript.h" #include <emscripten/emscripten.h> +#include <stdlib.h> -extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { +#include "godot_js.h" - String idbfs_err = String::utf8(p_idbfs_err); - if (!idbfs_err.empty()) { - print_line("IndexedDB not available: " + idbfs_err); +static OS_JavaScript *os = nullptr; +static uint64_t target_ticks = 0; + +void exit_callback() { + emscripten_cancel_main_loop(); // After this, we can exit! + Main::cleanup(); + int exit_code = OS_JavaScript::get_singleton()->get_exit_code(); + memdelete(os); + os = nullptr; + emscripten_force_exit(exit_code); // No matter that we call cancel_main_loop, regular "exit" will not work, forcing. +} + +void cleanup_after_sync() { + emscripten_set_main_loop(exit_callback, -1, false); +} + +void main_loop_callback() { + uint64_t current_ticks = os->get_ticks_usec(); + + bool force_draw = DisplayServerJavaScript::get_singleton()->check_size_force_redraw(); + if (force_draw) { + Main::force_redraw(); + } else if (current_ticks < target_ticks) { + return; // Skip frame. + } + + int target_fps = Engine::get_singleton()->get_target_fps(); + if (target_fps > 0) { + target_ticks += (uint64_t)(1000000 / target_fps); + } + if (os->main_loop_iterate()) { + emscripten_cancel_main_loop(); // Cancel current loop and wait for cleanup_after_sync. + godot_js_os_finish_async(cleanup_after_sync); } - OS_JavaScript *os = OS_JavaScript::get_singleton(); - os->set_idb_available(idbfs_err.empty()); - // Ease up compatibility. - ResourceLoader::set_abort_on_missing_resources(false); - Main::start(); - os->run_async(); } -int main(int argc, char *argv[]) { +/// When calling main, it is assumed FS is setup and synced. +extern EMSCRIPTEN_KEEPALIVE int godot_js_main(int argc, char *argv[]) { + os = new OS_JavaScript(); - // Sync from persistent state into memory and then - // run the 'main_after_fs_sync' function. - /* clang-format off */ - EM_ASM( - FS.mkdir('/userfs'); - FS.mount(IDBFS, {}, '/userfs'); - FS.syncfs(true, function(err) { - ccall('main_after_fs_sync', null, ['string'], [err ? err.message : ""]) - }); - ); - /* clang-format on */ + // We must override main when testing is enabled + TEST_MAIN_OVERRIDE - new OS_JavaScript(argc, argv); - // TODO: Check error return value. Main::setup(argv[0], argc - 1, &argv[1]); + // Ease up compatibility. + ResourceLoader::set_abort_on_missing_resources(false); + + Main::start(); + os->get_main_loop()->initialize(); + emscripten_set_main_loop(main_loop_callback, -1, false); + // Immediately run the first iteration. + // We are inside an animation frame, we want to immediately draw on the newly setup canvas. + main_loop_callback(); + return 0; - // Continued async in main_after_fs_sync() from the syncfs() callback. } diff --git a/platform/javascript/id_handler.js b/platform/javascript/javascript_runtime.cpp index 67d29075b8..2996e95a95 100644 --- a/platform/javascript/id_handler.js +++ b/platform/javascript/javascript_runtime.cpp @@ -1,12 +1,12 @@ /*************************************************************************/ -/* id_handler.js */ +/* javascript_runtime.cpp */ /*************************************************************************/ /* 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). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -28,36 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var IDHandler = /** @constructor */ function() { +extern int godot_js_main(int argc, char *argv[]); - var ids = {}; - var size = 0; - - this.has = function(id) { - return ids.hasOwnProperty(id); - } - - this.add = function(obj) { - size += 1; - var id = crypto.getRandomValues(new Int32Array(32))[0]; - ids[id] = obj; - return id; - } - - this.get = function(id) { - return ids[id]; - } - - this.remove = function(id) { - size -= 1; - delete ids[id]; - } - - this.size = function() { - return size; - } - - this.ids = ids; -}; - -Module.IDHandler = new IDHandler; +int main(int argc, char *argv[]) { + return godot_js_main(argc, argv); +} diff --git a/platform/javascript/js/dynlink.pre.js b/platform/javascript/js/dynlink.pre.js new file mode 100644 index 0000000000..34bc371ea9 --- /dev/null +++ b/platform/javascript/js/dynlink.pre.js @@ -0,0 +1 @@ +Module['dynamicLibraries'] = [Module['thisProgram'] + '.side.wasm'].concat(Module['dynamicLibraries'] ? Module['dynamicLibraries'] : []); diff --git a/platform/javascript/engine/externs.js b/platform/javascript/js/engine/engine.externs.js index 1a94dd15ec..1a94dd15ec 100644 --- a/platform/javascript/engine/externs.js +++ b/platform/javascript/js/engine/engine.externs.js diff --git a/platform/javascript/js/engine/engine.js b/platform/javascript/js/engine/engine.js new file mode 100644 index 0000000000..4b8a7dde69 --- /dev/null +++ b/platform/javascript/js/engine/engine.js @@ -0,0 +1,292 @@ +const Engine = (function () { + const preloader = new Preloader(); + + let wasmExt = '.wasm'; + let unloadAfterInit = true; + let loadPath = ''; + let loadPromise = null; + let initPromise = null; + let stderr = null; + let stdout = null; + let progressFunc = null; + + function load(basePath) { + if (loadPromise == null) { + loadPath = basePath; + loadPromise = preloader.loadPromise(basePath + wasmExt); + preloader.setProgressFunc(progressFunc); + requestAnimationFrame(preloader.animateProgress); + } + return loadPromise; + } + + function unload() { + loadPromise = null; + } + + /** @constructor */ + function Engine() { // eslint-disable-line no-shadow + this.canvas = null; + this.executableName = ''; + this.rtenv = null; + this.customLocale = null; + this.resizeCanvasOnStart = false; + this.onExecute = null; + this.onExit = null; + this.persistentPaths = ['/userfs']; + this.gdnativeLibs = []; + } + + Engine.prototype.init = /** @param {string=} basePath */ function (basePath) { + if (initPromise) { + return initPromise; + } + if (loadPromise == null) { + if (!basePath) { + initPromise = Promise.reject(new Error('A base path must be provided when calling `init` and the engine is not loaded.')); + return initPromise; + } + load(basePath); + } + let config = {}; + if (typeof stdout === 'function') { + config.print = stdout; + } + if (typeof stderr === 'function') { + config.printErr = stderr; + } + const me = this; + initPromise = new Promise(function (resolve, reject) { + config['locateFile'] = Utils.createLocateRewrite(loadPath); + config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); + // Emscripten configuration. + config['thisProgram'] = me.executableName; + config['noExitRuntime'] = true; + config['dynamicLibraries'] = me.gdnativeLibs; + Godot(config).then(function (module) { + module['initFS'](me.persistentPaths).then(function (fs_err) { + me.rtenv = module; + if (unloadAfterInit) { + unload(); + } + resolve(); + config = null; + }); + }); + }); + return initPromise; + }; + + /** @type {function(string, string):Object} */ + Engine.prototype.preloadFile = function (file, path) { + return preloader.preload(file, path); + }; + + /** @type {function(...string):Object} */ + Engine.prototype.start = function () { + // Start from arguments. + const args = []; + for (let i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + const me = this; + return me.init().then(function () { + if (!me.rtenv) { + return Promise.reject(new Error('The engine must be initialized before it can be started')); + } + + if (!(me.canvas instanceof HTMLCanvasElement)) { + me.canvas = Utils.findCanvas(); + if (!me.canvas) { + return Promise.reject(new Error('No canvas found in page')); + } + } + + // Canvas can grab focus on click, or key events won't work. + if (me.canvas.tabIndex < 0) { + me.canvas.tabIndex = 0; + } + + // Disable right-click context menu. + me.canvas.addEventListener('contextmenu', function (ev) { + ev.preventDefault(); + }, false); + + // Until context restoration is implemented warn the user of context loss. + me.canvas.addEventListener('webglcontextlost', function (ev) { + alert('WebGL context lost, please reload the page'); // eslint-disable-line no-alert + ev.preventDefault(); + }, false); + + // Browser locale, or custom one if defined. + let locale = me.customLocale; + if (!locale) { + locale = navigator.languages ? navigator.languages[0] : navigator.language; + locale = locale.split('.')[0]; + } + // Godot configuration. + me.rtenv['initConfig']({ + 'resizeCanvasOnStart': me.resizeCanvasOnStart, + 'canvas': me.canvas, + 'locale': locale, + 'onExecute': function (p_args) { + if (me.onExecute) { + me.onExecute(p_args); + return 0; + } + return 1; + }, + 'onExit': function (p_code) { + me.rtenv['deinitFS'](); + if (me.onExit) { + me.onExit(p_code); + } + me.rtenv = null; + }, + }); + + return new Promise(function (resolve, reject) { + preloader.preloadedFiles.forEach(function (file) { + me.rtenv['copyToFS'](file.path, file.buffer); + }); + preloader.preloadedFiles.length = 0; // Clear memory + me.rtenv['callMain'](args); + initPromise = null; + resolve(); + }); + }); + }; + + Engine.prototype.startGame = function (execName, mainPack, extraArgs) { + // Start and init with execName as loadPath if not inited. + this.executableName = execName; + const me = this; + return Promise.all([ + this.init(execName), + this.preloadFile(mainPack, mainPack), + ]).then(function () { + let args = ['--main-pack', mainPack]; + if (extraArgs) { + args = args.concat(extraArgs); + } + return me.start.apply(me, args); + }); + }; + + Engine.prototype.setWebAssemblyFilenameExtension = function (override) { + if (String(override).length === 0) { + throw new Error('Invalid WebAssembly filename extension override'); + } + wasmExt = String(override); + }; + + Engine.prototype.setUnloadAfterInit = function (enabled) { + unloadAfterInit = enabled; + }; + + Engine.prototype.setCanvas = function (canvasElem) { + this.canvas = canvasElem; + }; + + Engine.prototype.setCanvasResizedOnStart = function (enabled) { + this.resizeCanvasOnStart = enabled; + }; + + Engine.prototype.setLocale = function (locale) { + this.customLocale = locale; + }; + + Engine.prototype.setExecutableName = function (newName) { + this.executableName = newName; + }; + + Engine.prototype.setProgressFunc = function (func) { + progressFunc = func; + }; + + Engine.prototype.setStdoutFunc = function (func) { + const print = function (text) { + let msg = text; + if (arguments.length > 1) { + msg = Array.prototype.slice.call(arguments).join(' '); + } + func(msg); + }; + if (this.rtenv) { + this.rtenv.print = print; + } + stdout = print; + }; + + Engine.prototype.setStderrFunc = function (func) { + const printErr = function (text) { + let msg = text; + if (arguments.length > 1) { + msg = Array.prototype.slice.call(arguments).join(' '); + } + func(msg); + }; + if (this.rtenv) { + this.rtenv.printErr = printErr; + } + stderr = printErr; + }; + + Engine.prototype.setOnExecute = function (onExecute) { + this.onExecute = onExecute; + }; + + Engine.prototype.setOnExit = function (onExit) { + this.onExit = onExit; + }; + + Engine.prototype.copyToFS = function (path, buffer) { + if (this.rtenv == null) { + throw new Error('Engine must be inited before copying files'); + } + this.rtenv['copyToFS'](path, buffer); + }; + + Engine.prototype.setPersistentPaths = function (persistentPaths) { + this.persistentPaths = persistentPaths; + }; + + Engine.prototype.setGDNativeLibraries = function (gdnativeLibs) { + this.gdnativeLibs = gdnativeLibs; + }; + + 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['setGDNativeLibraries'] = Engine.prototype.setGDNativeLibraries; + Engine.prototype['requestQuit'] = Engine.prototype.requestQuit; + return Engine; +}()); +if (typeof window !== 'undefined') { + window['Engine'] = Engine; +} diff --git a/platform/javascript/js/engine/preloader.js b/platform/javascript/js/engine/preloader.js new file mode 100644 index 0000000000..ec34fb93f2 --- /dev/null +++ b/platform/javascript/js/engine/preloader.js @@ -0,0 +1,127 @@ +const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars + const loadXHR = function (resolve, reject, file, tracker, attempts) { + const xhr = new XMLHttpRequest(); + tracker[file] = { + total: 0, + loaded: 0, + final: false, + }; + xhr.onerror = function () { + if (attempts <= 1) { + reject(new Error(`Failed loading file '${file}'`)); + } else { + setTimeout(function () { + loadXHR(resolve, reject, file, tracker, attempts - 1); + }, 1000); + } + }; + xhr.onabort = function () { + tracker[file].final = true; + reject(new Error(`Loading file '${file}' was aborted.`)); + }; + xhr.onloadstart = function (ev) { + tracker[file].total = ev.total; + tracker[file].loaded = ev.loaded; + }; + xhr.onprogress = function (ev) { + tracker[file].loaded = ev.loaded; + tracker[file].total = ev.total; + }; + xhr.onload = function () { + if (xhr.status >= 400) { + if (xhr.status < 500 || attempts <= 1) { + reject(new Error(`Failed loading file '${file}': ${xhr.statusText}`)); + xhr.abort(); + } else { + setTimeout(function () { + loadXHR(resolve, reject, file, tracker, attempts - 1); + }, 1000); + } + } else { + tracker[file].final = true; + resolve(xhr); + } + }; + // Make request. + xhr.open('GET', file); + if (!file.endsWith('.js')) { + xhr.responseType = 'arraybuffer'; + } + xhr.send(); + }; + + const DOWNLOAD_ATTEMPTS_MAX = 4; + const loadingFiles = {}; + const lastProgress = { loaded: 0, total: 0 }; + let progressFunc = null; + + const animateProgress = function () { + let loaded = 0; + let total = 0; + let totalIsValid = true; + let progressIsFinal = true; + + Object.keys(loadingFiles).forEach(function (file) { + const stat = loadingFiles[file]; + if (!stat.final) { + progressIsFinal = false; + } + if (!totalIsValid || stat.total === 0) { + totalIsValid = false; + total = 0; + } else { + total += stat.total; + } + loaded += stat.loaded; + }); + if (loaded !== lastProgress.loaded || total !== lastProgress.total) { + lastProgress.loaded = loaded; + lastProgress.total = total; + if (typeof progressFunc === 'function') { + progressFunc(loaded, total); + } + } + if (!progressIsFinal) { + requestAnimationFrame(animateProgress); + } + }; + + this.animateProgress = animateProgress; + + this.setProgressFunc = function (callback) { + progressFunc = callback; + }; + + this.loadPromise = function (file) { + return new Promise(function (resolve, reject) { + loadXHR(resolve, reject, file, loadingFiles, DOWNLOAD_ATTEMPTS_MAX); + }); + }; + + this.preloadedFiles = []; + this.preload = function (pathOrBuffer, destPath) { + let buffer = null; + if (typeof pathOrBuffer === 'string') { + const me = this; + return this.loadPromise(pathOrBuffer).then(function (xhr) { + me.preloadedFiles.push({ + path: destPath || pathOrBuffer, + buffer: xhr.response, + }); + return Promise.resolve(); + }); + } else if (pathOrBuffer instanceof ArrayBuffer) { + buffer = new Uint8Array(pathOrBuffer); + } else if (ArrayBuffer.isView(pathOrBuffer)) { + buffer = new Uint8Array(pathOrBuffer.buffer); + } + if (buffer) { + this.preloadedFiles.push({ + path: destPath, + buffer: pathOrBuffer, + }); + return Promise.resolve(); + } + return Promise.reject(new Error('Invalid object for preloading')); + }; +}; diff --git a/platform/javascript/js/engine/utils.js b/platform/javascript/js/engine/utils.js new file mode 100644 index 0000000000..9273bbad42 --- /dev/null +++ b/platform/javascript/js/engine/utils.js @@ -0,0 +1,58 @@ +const Utils = { // eslint-disable-line no-unused-vars + + createLocateRewrite: function (execName) { + function rw(path) { + if (path.endsWith('.worker.js')) { + return `${execName}.worker.js`; + } else if (path.endsWith('.audio.worklet.js')) { + return `${execName}.audio.worklet.js`; + } else if (path.endsWith('.js')) { + return `${execName}.js`; + } else if (path.endsWith('.side.wasm')) { + return `${execName}.side.wasm`; + } else if (path.endsWith('.wasm')) { + return `${execName}.wasm`; + } + return path; + } + return rw; + }, + + createInstantiatePromise: function (wasmLoader) { + let loader = wasmLoader; + function instantiateWasm(imports, onSuccess) { + loader.then(function (xhr) { + WebAssembly.instantiate(xhr.response, imports).then(function (result) { + onSuccess(result['instance'], result['module']); + }); + }); + loader = null; + return {}; + } + + return instantiateWasm; + }, + + findCanvas: function () { + const nodes = document.getElementsByTagName('canvas'); + if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { + return nodes[0]; + } + return null; + }, + + isWebGLAvailable: function (majorVersion = 1) { + let testContext = false; + try { + const testCanvas = document.createElement('canvas'); + if (majorVersion === 1) { + testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); + } else if (majorVersion === 2) { + testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2'); + } + } catch (e) { + // Not available + } + return !!testContext; + }, +}; diff --git a/platform/javascript/js/libs/audio.worklet.js b/platform/javascript/js/libs/audio.worklet.js new file mode 100644 index 0000000000..6b3f80c6a9 --- /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-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +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..8e385e9176 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_audio.js @@ -0,0 +1,362 @@ +/*************************************************************************/ +/* library_godot_audio.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +const 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 0; // Already started. + } + function gotMediaInput(stream) { + try { + GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); + callback(GodotAudio.input); + } catch (e) { + GodotRuntime.error('Failed creaating input.', e); + } + } + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia({ + 'audio': true, + }).then(gotMediaInput, function (e) { + GodotRuntime.error('Error getting user media.', e); + }); + } else { + if (!navigator.getUserMedia) { + navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + } + if (!navigator.getUserMedia) { + GodotRuntime.error('getUserMedia not available.'); + return 1; + } + navigator.getUserMedia({ + 'audio': true, + }, gotMediaInput, function (e) { + GodotRuntime.print(e); + }); + } + return 0; + }, + + close_async: function (resolve, reject) { + const ctx = GodotAudio.ctx; + GodotAudio.ctx = null; + // Audio was not initialized. + if (!ctx) { + resolve(); + return; + } + // Remove latency callback + if (GodotAudio.interval) { + clearInterval(GodotAudio.interval); + GodotAudio.interval = 0; + } + // Disconnect input, if it was started. + if (GodotAudio.input) { + GodotAudio.input.disconnect(); + GodotAudio.input = null; + } + // Disconnect output + let closed = Promise.resolve(); + if (GodotAudio.driver) { + closed = GodotAudio.driver.close(); + } + closed.then(function () { + return ctx.close(); + }).then(function () { + ctx.onstatechange = null; + resolve(); + }).catch(function (e) { + ctx.onstatechange = null; + GodotRuntime.error('Error closing AudioContext', e); + resolve(); + }); + }, + }, + + godot_audio_is_available__sig: 'i', + godot_audio_is_available__proxy: 'sync', + godot_audio_is_available: function () { + if (!(window.AudioContext || window.webkitAudioContext)) { + return 0; + } + return 1; + }, + + godot_audio_init__sig: 'iiiii', + godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) { + const statechange = GodotRuntime.get_func(p_state_change); + const latencyupdate = GodotRuntime.get_func(p_latency_update); + return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate); + }, + + godot_audio_resume__sig: 'v', + godot_audio_resume: function () { + if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') { + GodotAudio.ctx.resume(); + } + }, + + godot_audio_capture_start__proxy: 'sync', + godot_audio_capture_start__sig: 'i', + godot_audio_capture_start: function () { + return GodotAudio.create_input(function (input) { + input.connect(GodotAudio.driver.get_node()); + }); + }, + + godot_audio_capture_stop__proxy: 'sync', + godot_audio_capture_stop__sig: 'v', + godot_audio_capture_stop: function () { + if (GodotAudio.input) { + const tracks = GodotAudio.input['mediaStream']['getTracks'](); + for (let i = 0; i < tracks.length; i++) { + tracks[i]['stop'](); + } + GodotAudio.input.disconnect(); + GodotAudio.input = null; + } + }, +}; + +autoAddDeps(GodotAudio, '$GodotAudio'); +mergeInto(LibraryManager.library, GodotAudio); + +/** + * The AudioWorklet API driver, used when threads are available. + */ +const GodotAudioWorklet = { + $GodotAudioWorklet__deps: ['$GodotAudio', '$GodotConfig'], + $GodotAudioWorklet: { + promise: null, + worklet: null, + + 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__sig: 'vi', + godot_audio_worklet_create: function (channels) { + GodotAudioWorklet.create(channels); + }, + + godot_audio_worklet_start__sig: 'viiiii', + godot_audio_worklet_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) { + const out_buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); + const in_buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); + const state = GodotRuntime.heapSub(HEAP32, p_state, 4); + GodotAudioWorklet.start(in_buffer, out_buffer, state); + }, + + godot_audio_worklet_state_wait__sig: 'iiii', + godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) { + Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout); + return Atomics.load(HEAP32, (p_state >> 2) + p_idx); + }, + + godot_audio_worklet_state_add__sig: 'iiii', + godot_audio_worklet_state_add: function (p_state, p_idx, p_value) { + return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value); + }, + + godot_audio_worklet_state_get__sig: 'iii', + godot_audio_worklet_state_get: function (p_state, p_idx) { + return Atomics.load(HEAP32, (p_state >> 2) + p_idx); + }, +}; + +autoAddDeps(GodotAudioWorklet, '$GodotAudioWorklet'); +mergeInto(LibraryManager.library, GodotAudioWorklet); + +/* + * The deprecated ScriptProcessorNode API, used when threads are disabled. + */ +const GodotAudioScript = { + $GodotAudioScript__deps: ['$GodotAudio'], + $GodotAudioScript: { + script: null, + + create: function (buffer_length, channel_count) { + GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); + GodotAudio.driver = GodotAudioScript; + return GodotAudioScript.script.bufferSize; + }, + + start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess) { + GodotAudioScript.script.onaudioprocess = function (event) { + // Read input + const inb = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); + const input = event.inputBuffer; + if (GodotAudio.input) { + const inlen = input.getChannelData(0).length; + for (let ch = 0; ch < 2; ch++) { + const data = input.getChannelData(ch); + for (let s = 0; s < inlen; s++) { + inb[s * 2 + ch] = data[s]; + } + } + } + + // Let Godot process the input/output. + onprocess(); + + // Write the output. + const outb = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); + const output = event.outputBuffer; + const channels = output.numberOfChannels; + for (let ch = 0; ch < channels; ch++) { + const data = output.getChannelData(ch); + // Loop through samples and assign computed values. + for (let sample = 0; sample < data.length; sample++) { + data[sample] = outb[sample * channels + ch]; + } + } + }; + GodotAudioScript.script.connect(GodotAudio.ctx.destination); + }, + + get_node: function () { + return GodotAudioScript.script; + }, + + close: function () { + return new Promise(function (resolve, reject) { + GodotAudioScript.script.disconnect(); + GodotAudioScript.script.onaudioprocess = null; + GodotAudioScript.script = null; + resolve(); + }); + }, + }, + + godot_audio_script_create__sig: 'iii', + godot_audio_script_create: function (buffer_length, channel_count) { + return GodotAudioScript.create(buffer_length, channel_count); + }, + + godot_audio_script_start__sig: 'viiiii', + godot_audio_script_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) { + const onprocess = GodotRuntime.get_func(p_cb); + GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess); + }, +}; + +autoAddDeps(GodotAudioScript, '$GodotAudioScript'); +mergeInto(LibraryManager.library, GodotAudioScript); diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js new file mode 100644 index 0000000000..d1f4d9595b --- /dev/null +++ b/platform/javascript/js/libs/library_godot_display.js @@ -0,0 +1,497 @@ +/*************************************************************************/ +/* library_godot_display.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +/* + * 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__sig: 'i', + godot_js_display_is_swap_ok_cancel: function () { + const win = (['Windows', 'Win64', 'Win32', 'WinCE']); + const plat = navigator.platform || ''; + if (win.indexOf(plat) !== -1) { + return 1; + } + return 0; + }, + + godot_js_display_alert__sig: 'vi', + godot_js_display_alert: function (p_text) { + window.alert(GodotRuntime.parseString(p_text)); // eslint-disable-line no-alert + }, + + godot_js_display_pixel_ratio_get__sig: 'f', + godot_js_display_pixel_ratio_get: function () { + return window.devicePixelRatio || 1; + }, + + /* + * Canvas + */ + godot_js_display_canvas_focus__sig: 'v', + godot_js_display_canvas_focus: function () { + GodotConfig.canvas.focus(); + }, + + godot_js_display_canvas_is_focused__sig: 'i', + godot_js_display_canvas_is_focused: function () { + return document.activeElement === GodotConfig.canvas; + }, + + godot_js_display_canvas_bounding_rect_position_get__sig: 'vii', + godot_js_display_canvas_bounding_rect_position_get: function (r_x, r_y) { + const brect = GodotConfig.canvas.getBoundingClientRect(); + GodotRuntime.setHeapValue(r_x, brect.x, 'i32'); + GodotRuntime.setHeapValue(r_y, brect.y, 'i32'); + }, + + /* + * Touchscreen + */ + godot_js_display_touchscreen_is_available__sig: 'i', + godot_js_display_touchscreen_is_available: function () { + return 'ontouchstart' in window; + }, + + /* + * Clipboard + */ + godot_js_display_clipboard_set__sig: 'ii', + godot_js_display_clipboard_set: function (p_text) { + const text = GodotRuntime.parseString(p_text); + if (!navigator.clipboard || !navigator.clipboard.writeText) { + return 1; + } + navigator.clipboard.writeText(text).catch(function (e) { + // Setting OS clipboard is only possible from an input callback. + GodotRuntime.error('Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:', e); + }); + return 0; + }, + + godot_js_display_clipboard_get__sig: 'ii', + godot_js_display_clipboard_get: function (callback) { + const func = GodotRuntime.get_func(callback); + try { + navigator.clipboard.readText().then(function (result) { + const ptr = GodotRuntime.allocString(result); + func(ptr); + GodotRuntime.free(ptr); + }).catch(function (e) { + // Fail graciously. + }); + } catch (e) { + // Fail graciously. + } + }, + + /* + * Window + */ + godot_js_display_window_request_fullscreen__sig: 'v', + godot_js_display_window_request_fullscreen: function () { + const canvas = GodotConfig.canvas; + (canvas.requestFullscreen || canvas.msRequestFullscreen + || canvas.mozRequestFullScreen || canvas.mozRequestFullscreen + || canvas.webkitRequestFullscreen + ).call(canvas); + }, + + godot_js_display_window_title_set__sig: 'vi', + godot_js_display_window_title_set: function (p_data) { + document.title = GodotRuntime.parseString(p_data); + }, + + godot_js_display_window_icon_set__sig: 'vii', + godot_js_display_window_icon_set: function (p_ptr, p_len) { + let link = document.getElementById('-gd-engine-icon'); + if (link === null) { + link = document.createElement('link'); + link.rel = 'icon'; + link.id = '-gd-engine-icon'; + document.head.appendChild(link); + } + const old_icon = GodotDisplay.window_icon; + const png = new Blob([GodotRuntime.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__sig: 'vi', + godot_js_display_cursor_set_visible: function (p_visible) { + const visible = p_visible !== 0; + if (visible === GodotDisplayCursor.visible) { + return; + } + GodotDisplayCursor.visible = visible; + if (visible) { + GodotDisplayCursor.set_shape(GodotDisplayCursor.shape); + } else { + GodotDisplayCursor.set_style('none'); + } + }, + + godot_js_display_cursor_is_hidden__sig: 'i', + godot_js_display_cursor_is_hidden: function () { + return !GodotDisplayCursor.visible; + }, + + godot_js_display_cursor_set_shape__sig: 'vi', + godot_js_display_cursor_set_shape: function (p_string) { + GodotDisplayCursor.set_shape(GodotRuntime.parseString(p_string)); + }, + + godot_js_display_cursor_set_custom_shape__sig: 'viiiii', + godot_js_display_cursor_set_custom_shape: function (p_shape, p_ptr, p_len, p_hotspot_x, p_hotspot_y) { + const shape = GodotRuntime.parseString(p_shape); + const old_shape = GodotDisplayCursor.cursors[shape]; + if (p_len > 0) { + const png = new Blob([GodotRuntime.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__sig: 'viiiii', + godot_js_display_notification_cb: function (callback, p_enter, p_exit, p_in, p_out) { + const canvas = GodotConfig.canvas; + const func = GodotRuntime.get_func(callback); + const notif = [p_enter, p_exit, p_in, p_out]; + ['mouseover', 'mouseleave', 'focus', 'blur'].forEach(function (evt_name, idx) { + GodotDisplayListeners.add(canvas, evt_name, function () { + func.bind(null, notif[idx]); + }, true); + }); + }, + + godot_js_display_paste_cb__sig: 'vi', + 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__sig: 'vi', + 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..d7f1ad5ea1 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_editor_tools.js @@ -0,0 +1,57 @@ +/*************************************************************************/ +/* library_godot_editor_tools.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +const GodotEditorTools = { + godot_js_editor_download_file__deps: ['$FS'], + godot_js_editor_download_file__sig: 'viii', + 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..9ab392b813 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_eval.js @@ -0,0 +1,86 @@ +/*************************************************************************/ +/* library_godot_eval.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +const GodotEval = { + godot_js_eval__deps: ['$GodotRuntime'], + godot_js_eval__sig: 'iiiiiii', + godot_js_eval: function (p_js, p_use_global_ctx, p_union_ptr, p_byte_arr, p_byte_arr_write, p_callback) { + const js_code = GodotRuntime.parseString(p_js); + let eval_ret = null; + try { + if (p_use_global_ctx) { + // indirect eval call grants global execution context + const global_eval = eval; // eslint-disable-line no-eval + eval_ret = global_eval(js_code); + } else { + eval_ret = eval(js_code); // eslint-disable-line no-eval + } + } catch (e) { + GodotRuntime.error(e); + } + + switch (typeof eval_ret) { + case 'boolean': + GodotRuntime.setHeapValue(p_union_ptr, eval_ret, 'i32'); + return 1; // BOOL + + case 'number': + GodotRuntime.setHeapValue(p_union_ptr, eval_ret, 'double'); + return 3; // REAL + + case 'string': + GodotRuntime.setHeapValue(p_union_ptr, GodotRuntime.allocString(eval_ret), '*'); + return 4; // STRING + + case 'object': + if (eval_ret === null) { + break; + } + + if (ArrayBuffer.isView(eval_ret) && !(eval_ret instanceof Uint8Array)) { + eval_ret = new Uint8Array(eval_ret.buffer); + } else if (eval_ret instanceof ArrayBuffer) { + eval_ret = new Uint8Array(eval_ret); + } + if (eval_ret instanceof Uint8Array) { + const func = GodotRuntime.get_func(p_callback); + const bytes_ptr = func(p_byte_arr, p_byte_arr_write, eval_ret.length); + HEAPU8.set(eval_ret, bytes_ptr); + return 20; // POOL_BYTE_ARRAY + } + break; + + // no default + } + return 0; // NIL + }, +}; + +mergeInto(LibraryManager.library, GodotEval); diff --git a/platform/javascript/http_request.js b/platform/javascript/js/libs/library_godot_http_request.js index f621689f9d..930d3209f8 100644 --- a/platform/javascript/http_request.js +++ b/platform/javascript/js/libs/library_godot_http_request.js @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -27,120 +27,134 @@ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var GodotHTTPRequest = { +const GodotHTTPRequest = { + $GodotHTTPRequest__deps: ['$GodotRuntime'], $GodotHTTPRequest: { - requests: [], - getUnusedRequestId: function() { - var idMax = GodotHTTPRequest.requests.length; - for (var potentialId = 0; potentialId < idMax; ++potentialId) { + getUnusedRequestId: function () { + const idMax = GodotHTTPRequest.requests.length; + for (let potentialId = 0; potentialId < idMax; ++potentialId) { if (GodotHTTPRequest.requests[potentialId] instanceof XMLHttpRequest) { continue; } return potentialId; } - GodotHTTPRequest.requests.push(null) + GodotHTTPRequest.requests.push(null); return idMax; }, - setupRequest: function(xhr) { + setupRequest: function (xhr) { xhr.responseType = 'arraybuffer'; }, }, - godot_xhr_new: function() { - var newId = GodotHTTPRequest.getUnusedRequestId(); - GodotHTTPRequest.requests[newId] = new XMLHttpRequest; + godot_xhr_new__sig: 'i', + godot_xhr_new: function () { + const newId = GodotHTTPRequest.getUnusedRequestId(); + GodotHTTPRequest.requests[newId] = new XMLHttpRequest(); GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[newId]); return newId; }, - godot_xhr_reset: function(xhrId) { - GodotHTTPRequest.requests[xhrId] = new XMLHttpRequest; + godot_xhr_reset__sig: 'vi', + godot_xhr_reset: function (xhrId) { + GodotHTTPRequest.requests[xhrId] = new XMLHttpRequest(); GodotHTTPRequest.setupRequest(GodotHTTPRequest.requests[xhrId]); }, - godot_xhr_free: function(xhrId) { + godot_xhr_free__sig: 'vi', + 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_open__sig: 'viiiii', + godot_xhr_open: function (xhrId, method, url, p_user, p_password) { + const user = p_user > 0 ? GodotRuntime.parseString(p_user) : null; + const password = p_password > 0 ? GodotRuntime.parseString(p_password) : null; + GodotHTTPRequest.requests[xhrId].open(GodotRuntime.parseString(method), GodotRuntime.parseString(url), true, user, password); }, - godot_xhr_set_request_header: function(xhrId, header, value) { - GodotHTTPRequest.requests[xhrId].setRequestHeader(UTF8ToString(header), UTF8ToString(value)); + godot_xhr_set_request_header__sig: 'viii', + godot_xhr_set_request_header: function (xhrId, header, value) { + GodotHTTPRequest.requests[xhrId].setRequestHeader(GodotRuntime.parseString(header), GodotRuntime.parseString(value)); }, - godot_xhr_send_null: function(xhrId) { + godot_xhr_send_null__sig: 'vi', + godot_xhr_send_null: function (xhrId) { GodotHTTPRequest.requests[xhrId].send(); }, - godot_xhr_send_string: function(xhrId, strPtr) { + godot_xhr_send_string__sig: 'vii', + godot_xhr_send_string: function (xhrId, strPtr) { if (!strPtr) { - err("Failed to send string per XHR: null pointer"); + GodotRuntime.error('Failed to send string per XHR: null pointer'); return; } - GodotHTTPRequest.requests[xhrId].send(UTF8ToString(strPtr)); + GodotHTTPRequest.requests[xhrId].send(GodotRuntime.parseString(strPtr)); }, - godot_xhr_send_data: function(xhrId, ptr, len) { + godot_xhr_send_data__sig: 'viii', + godot_xhr_send_data: function (xhrId, ptr, len) { if (!ptr) { - err("Failed to send data per XHR: null pointer"); + GodotRuntime.error('Failed to send data per XHR: null pointer'); return; } if (len < 0) { - err("Failed to send data per XHR: buffer length less than 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) { + godot_xhr_abort__sig: 'vi', + godot_xhr_abort: function (xhrId) { GodotHTTPRequest.requests[xhrId].abort(); }, - godot_xhr_get_status: function(xhrId) { + godot_xhr_get_status__sig: 'ii', + godot_xhr_get_status: function (xhrId) { return GodotHTTPRequest.requests[xhrId].status; }, - godot_xhr_get_ready_state: function(xhrId) { + godot_xhr_get_ready_state__sig: 'ii', + godot_xhr_get_ready_state: function (xhrId) { return GodotHTTPRequest.requests[xhrId].readyState; }, - godot_xhr_get_response_headers_length: function(xhrId) { - var headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); - return headers === null ? 0 : lengthBytesUTF8(headers); + godot_xhr_get_response_headers_length__sig: 'ii', + godot_xhr_get_response_headers_length: function (xhrId) { + const headers = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); + return headers === null ? 0 : GodotRuntime.strlen(headers); }, - godot_xhr_get_response_headers: function(xhrId, dst, len) { - var str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); - if (str === null) + godot_xhr_get_response_headers__sig: 'viii', + godot_xhr_get_response_headers: function (xhrId, dst, len) { + const str = GodotHTTPRequest.requests[xhrId].getAllResponseHeaders(); + if (str === null) { return; - var buf = new Uint8Array(len + 1); - stringToUTF8Array(str, buf, 0, buf.length); - buf = buf.subarray(0, -1); - HEAPU8.set(buf, dst); + } + GodotRuntime.stringToHeap(str, dst, len); }, - godot_xhr_get_response_length: function(xhrId) { - var body = GodotHTTPRequest.requests[xhrId].response; + godot_xhr_get_response_length__sig: 'ii', + godot_xhr_get_response_length: function (xhrId) { + const body = GodotHTTPRequest.requests[xhrId].response; return body === null ? 0 : body.byteLength; }, - godot_xhr_get_response: function(xhrId, dst, len) { - var buf = GodotHTTPRequest.requests[xhrId].response; - if (buf === null) + godot_xhr_get_response__sig: 'viii', + godot_xhr_get_response: function (xhrId, dst, len) { + let buf = GodotHTTPRequest.requests[xhrId].response; + if (buf === null) { return; + } buf = new Uint8Array(buf).subarray(0, len); HEAPU8.set(buf, dst); }, }; -autoAddDeps(GodotHTTPRequest, "$GodotHTTPRequest"); +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..9fde4a84e1 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_os.js @@ -0,0 +1,288 @@ +/*************************************************************************/ +/* library_godot_os.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +const IDHandler = { + $IDHandler: { + _last_id: 0, + _references: {}, + + get: function (p_id) { + return IDHandler._references[p_id]; + }, + + add: function (p_data) { + const id = ++IDHandler._last_id; + IDHandler._references[id] = p_data; + return id; + }, + + remove: function (p_id) { + delete IDHandler._references[p_id]; + }, + }, +}; + +autoAddDeps(IDHandler, '$IDHandler'); +mergeInto(LibraryManager.library, IDHandler); + +const GodotConfig = { + $GodotConfig__postset: 'Module["initConfig"] = GodotConfig.init_config;', + $GodotConfig__deps: ['$GodotRuntime'], + $GodotConfig: { + canvas: null, + locale: 'en', + resize_on_start: false, + on_execute: null, + + init_config: function (p_opts) { + GodotConfig.resize_on_start = !!p_opts['resizeCanvasOnStart']; + GodotConfig.canvas = p_opts['canvas']; + GodotConfig.locale = p_opts['locale'] || GodotConfig.locale; + GodotConfig.on_execute = p_opts['onExecute']; + // This is called by emscripten, even if undocumented. + Module['onExit'] = p_opts['onExit']; // eslint-disable-line no-undef + }, + + locate_file: function (file) { + return Module['locateFile'](file); // eslint-disable-line no-undef + }, + }, + + godot_js_config_canvas_id_get__sig: 'vii', + godot_js_config_canvas_id_get: function (p_ptr, p_ptr_max) { + GodotRuntime.stringToHeap(`#${GodotConfig.canvas.id}`, p_ptr, p_ptr_max); + }, + + godot_js_config_locale_get__sig: 'vii', + godot_js_config_locale_get: function (p_ptr, p_ptr_max) { + GodotRuntime.stringToHeap(GodotConfig.locale, p_ptr, p_ptr_max); + }, + + godot_js_config_is_resize_on_start__sig: 'i', + godot_js_config_is_resize_on_start: function () { + return GodotConfig.resize_on_start ? 1 : 0; + }, +}; + +autoAddDeps(GodotConfig, '$GodotConfig'); +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)); + }, + }, +}; +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__sig: 'vi', + godot_js_os_finish_async: function (p_callback) { + const func = GodotRuntime.get_func(p_callback); + GodotOS.finish_async(func); + }, + + godot_js_os_request_quit_cb__sig: 'vi', + godot_js_os_request_quit_cb: function (p_callback) { + GodotOS.request_quit = GodotRuntime.get_func(p_callback); + }, + + godot_js_os_fs_is_persistent__sig: 'i', + godot_js_os_fs_is_persistent: function () { + return GodotFS.is_persistent(); + }, + + godot_js_os_fs_sync__sig: 'vi', + godot_js_os_fs_sync: function (callback) { + const func = GodotRuntime.get_func(callback); + GodotOS._fs_sync_promise = GodotFS.sync(); + GodotOS._fs_sync_promise.then(function (err) { + func(); + }); + }, + + godot_js_os_execute__sig: 'ii', + godot_js_os_execute: function (p_json) { + const json_args = GodotRuntime.parseString(p_json); + const args = JSON.parse(json_args); + if (GodotConfig.on_execute) { + GodotConfig.on_execute(args); + return 0; + } + return 1; + }, + + godot_js_os_shell_open__sig: 'vi', + godot_js_os_shell_open: function (p_uri) { + window.open(GodotRuntime.parseString(p_uri), '_blank'); + }, +}; + +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..7e36ff8ab5 --- /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-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +const 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/os_javascript.cpp b/platform/javascript/os_javascript.cpp index c0230b94fa..b922b2ba91 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,1266 +30,203 @@ #include "os_javascript.h" -#include "core/io/file_access_buffered_fa.h" -//#include "drivers/gles2/rasterizer_gles2.h" -#include "drivers/dummy/rasterizer_dummy.h" +#include "core/debugger/engine_debugger.h" +#include "core/io/json.h" #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "main/main.h" -#include "servers/rendering/rendering_server_raster.h" +#include "modules/modules_enabled.gen.h" +#include "platform/javascript/display_server_javascript.h" +#ifdef MODULE_WEBSOCKET_ENABLED +#include "modules/websocket/remote_debugger_peer_websocket.h" +#endif + +#include <dlfcn.h> #include <emscripten.h> -#include <png.h> #include <stdlib.h> -#include "dom_keys.inc" - -#define DOM_BUTTON_LEFT 0 -#define DOM_BUTTON_MIDDLE 1 -#define DOM_BUTTON_RIGHT 2 -#define DOM_BUTTON_XBUTTON1 3 -#define DOM_BUTTON_XBUTTON2 4 -#define GODOT_CANVAS_SELECTOR "#canvas" - -// Window (canvas) - -static void focus_canvas() { - - /* clang-format off */ - EM_ASM( - Module.canvas.focus(); - ); - /* clang-format on */ -} - -static bool is_canvas_focused() { - - /* clang-format off */ - return EM_ASM_INT_V( - return document.activeElement == Module.canvas; - ); - /* clang-format on */ -} - -static Point2 compute_position_in_canvas(int x, int y) { - int canvas_x = EM_ASM_INT({ - return document.getElementById('canvas').getBoundingClientRect().x; - }); - int canvas_y = EM_ASM_INT({ - return document.getElementById('canvas').getBoundingClientRect().y; - }); - int canvas_width; - int canvas_height; - emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, &canvas_width, &canvas_height); - - double element_width; - double element_height; - emscripten_get_element_css_size(GODOT_CANVAS_SELECTOR, &element_width, &element_height); - - return Point2((int)(canvas_width / element_width * (x - canvas_x)), - (int)(canvas_height / element_height * (y - canvas_y))); -} - -static bool cursor_inside_canvas = true; - -EM_BOOL OS_JavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - // Empty ID is canvas. - String target_id = String::utf8(p_event->id); - if (target_id.empty() || target_id == "canvas") { - // This event property is the only reliable data on - // browser fullscreen state. - os->video_mode.fullscreen = p_event->isFullscreen; - if (os->video_mode.fullscreen) { - os->entering_fullscreen = false; - } else { - // Restoring maximized window now will cause issues, - // so delay until main_loop_iterate. - os->just_exited_fullscreen = true; - } - } - return false; -} - -void OS_JavaScript::set_video_mode(const VideoMode &p_video_mode, int p_screen) { - - video_mode = p_video_mode; -} - -OS::VideoMode OS_JavaScript::get_video_mode(int p_screen) const { - - return video_mode; -} - -Size2 OS_JavaScript::get_screen_size(int p_screen) const { - - EmscriptenFullscreenChangeEvent ev; - EMSCRIPTEN_RESULT result = emscripten_get_fullscreen_status(&ev); - ERR_FAIL_COND_V(result != EMSCRIPTEN_RESULT_SUCCESS, Size2()); - return Size2(ev.screenWidth, ev.screenHeight); -} - -void OS_JavaScript::set_window_size(const Size2 p_size) { - - windowed_size = p_size; - if (video_mode.fullscreen) { - window_maximized = false; - set_window_fullscreen(false); - } else { - if (window_maximized) { - emscripten_exit_soft_fullscreen(); - window_maximized = false; - } - emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, p_size.x, p_size.y); - } -} - -Size2 OS_JavaScript::get_window_size() const { - - int canvas[2]; - emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, canvas, canvas + 1); - return Size2(canvas[0], canvas[1]); -} - -void OS_JavaScript::set_window_maximized(bool p_enabled) { - - if (video_mode.fullscreen) { - window_maximized = p_enabled; - set_window_fullscreen(false); - } else if (!p_enabled) { - emscripten_exit_soft_fullscreen(); - window_maximized = false; - } else if (!window_maximized) { - // Prevent calling emscripten_enter_soft_fullscreen mutltiple times, - // this would hide page elements permanently. - EmscriptenFullscreenStrategy strategy; - strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; - strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; - strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = nullptr; - emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy); - window_maximized = p_enabled; - } -} - -bool OS_JavaScript::is_window_maximized() const { - - return window_maximized; -} - -void OS_JavaScript::set_window_fullscreen(bool p_enabled) { - - if (p_enabled == video_mode.fullscreen) { - return; - } - - // Just request changes here, if successful, logic continues in - // fullscreen_change_callback. - if (p_enabled) { - if (window_maximized) { - // Soft fullsreen during real fullscreen can cause issues, so exit. - // This must be called before requesting full screen. - emscripten_exit_soft_fullscreen(); - } - EmscriptenFullscreenStrategy strategy; - strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; - strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; - strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = nullptr; - EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(GODOT_CANVAS_SELECTOR, false, &strategy); - ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "Enabling fullscreen is only possible from an input callback for the HTML5 platform."); - ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "Enabling fullscreen is only possible from an input callback for the HTML5 platform."); - // Not fullscreen yet, so prevent "windowed" canvas dimensions from - // being overwritten. - entering_fullscreen = true; - } else { - // No logic allowed here, since exiting w/ ESC key won't use this function. - ERR_FAIL_COND(emscripten_exit_fullscreen() != EMSCRIPTEN_RESULT_SUCCESS); - } -} - -bool OS_JavaScript::is_window_fullscreen() const { - - return video_mode.fullscreen; -} - -void OS_JavaScript::get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen) const { - - Size2 screen = get_screen_size(); - p_list->push_back(OS::VideoMode(screen.width, screen.height, true)); -} - -bool OS_JavaScript::get_window_per_pixel_transparency_enabled() const { - if (!is_layered_allowed()) { - return false; - } - return transparency_enabled; -} - -void OS_JavaScript::set_window_per_pixel_transparency_enabled(bool p_enabled) { - if (!is_layered_allowed()) { - return; - } - transparency_enabled = p_enabled; -} - -// Keys - -template <typename T> -static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event) { - - godot_event->set_shift(emscripten_event_ptr->shiftKey); - godot_event->set_alt(emscripten_event_ptr->altKey); - godot_event->set_control(emscripten_event_ptr->ctrlKey); - godot_event->set_metakey(emscripten_event_ptr->metaKey); -} - -static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) { - - Ref<InputEventKey> ev; - ev.instance(); - ev->set_echo(emscripten_event->repeat); - dom2godot_mod(emscripten_event, ev); - ev->set_keycode(dom2godot_keycode(emscripten_event->keyCode)); - ev->set_physical_keycode(dom2godot_keycode(emscripten_event->keyCode)); - - String unicode = String::utf8(emscripten_event->key); - // Check if empty or multi-character (e.g. `CapsLock`). - if (unicode.length() != 1) { - // Might be empty as well, but better than nonsense. - unicode = String::utf8(emscripten_event->charValue); - } - if (unicode.length() == 1) { - ev->set_unicode(unicode[0]); - } - - return ev; -} - -EM_BOOL OS_JavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - Ref<InputEventKey> ev = setup_key_event(p_event); - ev->set_pressed(true); - if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_keycode())) { - // Defer to keypress event for legacy unicode retrieval. - os->deferred_key_event = ev; - // Do not suppress keypress event. - return false; - } - os->input->parse_input_event(ev); - // Resume audio context after input in case autoplay was denied. - os->audio_driver_javascript.resume(); - return true; -} - -EM_BOOL OS_JavaScript::keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - os->deferred_key_event->set_unicode(p_event->charCode); - os->input->parse_input_event(os->deferred_key_event); - return true; -} - -EM_BOOL OS_JavaScript::keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - - Ref<InputEventKey> ev = setup_key_event(p_event); - ev->set_pressed(false); - get_singleton()->input->parse_input_event(ev); - return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != 0; -} - -// Mouse - -Point2 OS_JavaScript::get_mouse_position() const { - - return input->get_mouse_position(); -} - -int OS_JavaScript::get_mouse_button_state() const { - - return input->get_mouse_button_mask(); -} - -EM_BOOL OS_JavaScript::mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - - Ref<InputEventMouseButton> ev; - ev.instance(); - ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_MOUSEDOWN); - ev->set_position(compute_position_in_canvas(p_event->clientX, p_event->clientY)); - ev->set_global_position(ev->get_position()); - dom2godot_mod(p_event, ev); - - switch (p_event->button) { - case DOM_BUTTON_LEFT: ev->set_button_index(BUTTON_LEFT); break; - case DOM_BUTTON_MIDDLE: ev->set_button_index(BUTTON_MIDDLE); break; - case DOM_BUTTON_RIGHT: ev->set_button_index(BUTTON_RIGHT); break; - case DOM_BUTTON_XBUTTON1: ev->set_button_index(BUTTON_XBUTTON1); break; - case DOM_BUTTON_XBUTTON2: ev->set_button_index(BUTTON_XBUTTON2); break; - default: return false; - } - - if (ev->is_pressed()) { - - double diff = emscripten_get_now() - os->last_click_ms; - - if (ev->get_button_index() == os->last_click_button_index) { - - if (diff < 400 && Point2(os->last_click_pos).distance_to(ev->get_position()) < 5) { - - os->last_click_ms = 0; - os->last_click_pos = Point2(-100, -100); - os->last_click_button_index = -1; - ev->set_doubleclick(true); - } - - } else { - os->last_click_button_index = ev->get_button_index(); - } - - if (!ev->is_doubleclick()) { - os->last_click_ms += diff; - os->last_click_pos = ev->get_position(); - } - } - - int mask = os->input->get_mouse_button_mask(); - int button_flag = 1 << (ev->get_button_index() - 1); - if (ev->is_pressed()) { - // Since the event is consumed, focus manually. The containing iframe, - // if exists, may not have focus yet, so focus even if already focused. - focus_canvas(); - mask |= button_flag; - } else if (mask & button_flag) { - mask &= ~button_flag; - } else { - // Received release event, but press was outside the canvas, so ignore. - return false; - } - ev->set_button_mask(mask); - - os->input->parse_input_event(ev); - // Resume audio context after input in case autoplay was denied. - os->audio_driver_javascript.resume(); - // Prevent multi-click text selection and wheel-click scrolling anchor. - // Context menu is prevented through contextmenu event. - return true; -} - -EM_BOOL OS_JavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - - int input_mask = os->input->get_mouse_button_mask(); - Point2 pos = compute_position_in_canvas(p_event->clientX, p_event->clientY); - // For motion outside the canvas, only read mouse movement if dragging - // started inside the canvas; imitating desktop app behaviour. - if (!cursor_inside_canvas && !input_mask) - return false; - - Ref<InputEventMouseMotion> ev; - ev.instance(); - dom2godot_mod(p_event, ev); - ev->set_button_mask(input_mask); - - ev->set_position(pos); - ev->set_global_position(ev->get_position()); - - ev->set_relative(Vector2(p_event->movementX, p_event->movementY)); - os->input->set_mouse_position(ev->get_position()); - ev->set_speed(os->input->get_last_mouse_speed()); - - os->input->parse_input_event(ev); - // Don't suppress mouseover/-leave events. - return false; -} - -static const char *godot2dom_cursor(OS::CursorShape p_shape) { - - switch (p_shape) { - case OS::CURSOR_ARROW: - default: - return "auto"; - case OS::CURSOR_IBEAM: return "text"; - case OS::CURSOR_POINTING_HAND: return "pointer"; - case OS::CURSOR_CROSS: return "crosshair"; - case OS::CURSOR_WAIT: return "progress"; - case OS::CURSOR_BUSY: return "wait"; - case OS::CURSOR_DRAG: return "grab"; - case OS::CURSOR_CAN_DROP: return "grabbing"; - case OS::CURSOR_FORBIDDEN: return "no-drop"; - case OS::CURSOR_VSIZE: return "ns-resize"; - case OS::CURSOR_HSIZE: return "ew-resize"; - case OS::CURSOR_BDIAGSIZE: return "nesw-resize"; - case OS::CURSOR_FDIAGSIZE: return "nwse-resize"; - case OS::CURSOR_MOVE: return "move"; - case OS::CURSOR_VSPLIT: return "row-resize"; - case OS::CURSOR_HSPLIT: return "col-resize"; - case OS::CURSOR_HELP: return "help"; - } -} - -static void set_css_cursor(const char *p_cursor) { - - /* clang-format off */ - EM_ASM_({ - Module.canvas.style.cursor = UTF8ToString($0); - }, p_cursor); - /* clang-format on */ -} - -static bool is_css_cursor_hidden() { - - /* clang-format off */ - return EM_ASM_INT({ - return Module.canvas.style.cursor === 'none'; - }); - /* clang-format on */ -} - -void OS_JavaScript::set_cursor_shape(CursorShape p_shape) { - - ERR_FAIL_INDEX(p_shape, CURSOR_MAX); - - if (get_mouse_mode() == MOUSE_MODE_VISIBLE) { - if (cursors[p_shape] != "") { - Vector<String> url = cursors[p_shape].split("?"); - set_css_cursor(("url(\"" + url[0] + "\") " + url[1] + ", auto").utf8()); - } else { - set_css_cursor(godot2dom_cursor(p_shape)); - } - } - - cursor_shape = p_shape; -} - -void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { - - if (p_cursor.is_valid()) { - - Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); - - if (cursor_c) { - if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) { - set_cursor_shape(p_shape); - return; - } - - cursors_cache.erase(p_shape); - } - - Ref<Texture2D> texture = p_cursor; - Ref<AtlasTexture> atlas_texture = p_cursor; - Ref<Image> image; - Size2 texture_size; - Rect2 atlas_rect; - - if (texture.is_valid()) { - image = texture->get_data(); - if (image.is_valid()) { - image->duplicate(); - } - } - - if (!image.is_valid() && atlas_texture.is_valid()) { - texture = atlas_texture->get_atlas(); - - atlas_rect.size.width = texture->get_width(); - atlas_rect.size.height = texture->get_height(); - atlas_rect.position.x = atlas_texture->get_region().position.x; - atlas_rect.position.y = atlas_texture->get_region().position.y; - - texture_size.width = atlas_texture->get_region().size.x; - texture_size.height = atlas_texture->get_region().size.y; - } else if (image.is_valid()) { - texture_size.width = texture->get_width(); - texture_size.height = texture->get_height(); - } - - ERR_FAIL_COND(!texture.is_valid()); - ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); - ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); - ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - - image = texture->get_data(); - - ERR_FAIL_COND(!image.is_valid()); - - image = image->duplicate(); - - if (atlas_texture.is_valid()) - image->crop_from_point( - atlas_rect.position.x, - atlas_rect.position.y, - texture_size.width, - texture_size.height); - - if (image->get_format() != Image::FORMAT_RGBA8) { - image->convert(Image::FORMAT_RGBA8); - } - - png_image png_meta; - memset(&png_meta, 0, sizeof png_meta); - png_meta.version = PNG_IMAGE_VERSION; - png_meta.width = texture_size.width; - png_meta.height = texture_size.height; - png_meta.format = PNG_FORMAT_RGBA; - - PackedByteArray png; - size_t len; - PackedByteArray data = image->get_data(); - ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); - - png.resize(len); - ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); - - char *object_url; - /* clang-format off */ - EM_ASM({ - var PNG_PTR = $0; - var PNG_LEN = $1; - var PTR = $2; - - var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: 'image/png' }); - var url = URL.createObjectURL(png); - var length_bytes = lengthBytesUTF8(url) + 1; - var string_on_wasm_heap = _malloc(length_bytes); - setValue(PTR, string_on_wasm_heap, '*'); - stringToUTF8(url, string_on_wasm_heap, length_bytes); - }, png.ptr(), len, &object_url); - /* clang-format on */ - - String url = String::utf8(object_url) + "?" + itos(p_hotspot.x) + " " + itos(p_hotspot.y); - - /* clang-format off */ - EM_ASM({ _free($0); }, object_url); - /* clang-format on */ - - if (cursors[p_shape] != "") { - /* clang-format off */ - EM_ASM({ - URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); - }, cursors[p_shape].utf8().get_data()); - /* clang-format on */ - cursors[p_shape] = ""; - } - - cursors[p_shape] = url; - - Vector<Variant> params; - params.push_back(p_cursor); - params.push_back(p_hotspot); - cursors_cache.insert(p_shape, params); - - } else if (cursors[p_shape] != "") { - /* clang-format off */ - EM_ASM({ - URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); - }, cursors[p_shape].utf8().get_data()); - /* clang-format on */ - cursors[p_shape] = ""; - - cursors_cache.erase(p_shape); - } - - set_cursor_shape(cursor_shape); -} - -void OS_JavaScript::set_mouse_mode(OS::MouseMode p_mode) { - - ERR_FAIL_COND_MSG(p_mode == MOUSE_MODE_CONFINED, "MOUSE_MODE_CONFINED is not supported for the HTML5 platform."); - if (p_mode == get_mouse_mode()) - return; - - if (p_mode == MOUSE_MODE_VISIBLE) { - - // set_css_cursor must be called before set_cursor_shape to make the cursor visible - set_css_cursor(godot2dom_cursor(cursor_shape)); - set_cursor_shape(cursor_shape); - emscripten_exit_pointerlock(); - - } else if (p_mode == MOUSE_MODE_HIDDEN) { - - set_css_cursor("none"); - emscripten_exit_pointerlock(); - - } else if (p_mode == MOUSE_MODE_CAPTURED) { - - EMSCRIPTEN_RESULT result = emscripten_request_pointerlock("canvas", false); - ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); - ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); - // set_css_cursor must be called before set_cursor_shape to make the cursor visible - set_css_cursor(godot2dom_cursor(cursor_shape)); - set_cursor_shape(cursor_shape); - } -} - -OS::MouseMode OS_JavaScript::get_mouse_mode() const { - - if (is_css_cursor_hidden()) - return MOUSE_MODE_HIDDEN; - - EmscriptenPointerlockChangeEvent ev; - emscripten_get_pointerlock_status(&ev); - return (ev.isActive && String::utf8(ev.id) == "canvas") ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; -} - -// Wheel - -EM_BOOL OS_JavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data) { - - ERR_FAIL_COND_V(p_event_type != EMSCRIPTEN_EVENT_WHEEL, false); - if (!is_canvas_focused()) { - if (cursor_inside_canvas) { - focus_canvas(); - } else { - return false; - } - } - - InputDefault *input = get_singleton()->input; - Ref<InputEventMouseButton> ev; - ev.instance(); - ev->set_position(input->get_mouse_position()); - ev->set_global_position(ev->get_position()); - - ev->set_shift(input->is_key_pressed(KEY_SHIFT)); - ev->set_alt(input->is_key_pressed(KEY_ALT)); - ev->set_control(input->is_key_pressed(KEY_CONTROL)); - ev->set_metakey(input->is_key_pressed(KEY_META)); - - if (p_event->deltaY < 0) - ev->set_button_index(BUTTON_WHEEL_UP); - else if (p_event->deltaY > 0) - ev->set_button_index(BUTTON_WHEEL_DOWN); - else if (p_event->deltaX > 0) - ev->set_button_index(BUTTON_WHEEL_LEFT); - else if (p_event->deltaX < 0) - ev->set_button_index(BUTTON_WHEEL_RIGHT); - else - return false; - - // Different browsers give wildly different delta values, and we can't - // interpret deltaMode, so use default value for wheel events' factor. - - int button_flag = 1 << (ev->get_button_index() - 1); - - ev->set_pressed(true); - ev->set_button_mask(input->get_mouse_button_mask() | button_flag); - input->parse_input_event(ev); - - ev->set_pressed(false); - ev->set_button_mask(input->get_mouse_button_mask() & ~button_flag); - input->parse_input_event(ev); - - return true; -} - -// Touch - -bool OS_JavaScript::has_touchscreen_ui_hint() const { - - /* clang-format off */ - return EM_ASM_INT_V( - return 'ontouchstart' in window; - ); - /* clang-format on */ -} - -EM_BOOL OS_JavaScript::touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - Ref<InputEventScreenTouch> ev; - ev.instance(); - int lowest_id_index = -1; - for (int i = 0; i < p_event->numTouches; ++i) { - - const EmscriptenTouchPoint &touch = p_event->touches[i]; - if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) - lowest_id_index = i; - if (!touch.isChanged) - continue; - ev->set_index(touch.identifier); - ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); - os->touches[i] = ev->get_position(); - ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_TOUCHSTART); - - os->input->parse_input_event(ev); - } - // Resume audio context after input in case autoplay was denied. - os->audio_driver_javascript.resume(); - return true; -} - -EM_BOOL OS_JavaScript::touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - Ref<InputEventScreenDrag> ev; - ev.instance(); - int lowest_id_index = -1; - for (int i = 0; i < p_event->numTouches; ++i) { - - const EmscriptenTouchPoint &touch = p_event->touches[i]; - if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) - lowest_id_index = i; - if (!touch.isChanged) - continue; - ev->set_index(touch.identifier); - ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); - Point2 &prev = os->touches[i]; - ev->set_relative(ev->get_position() - prev); - prev = ev->get_position(); - - os->input->parse_input_event(ev); - } - return true; -} - -// Gamepad - -EM_BOOL OS_JavaScript::gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data) { - - InputDefault *input = get_singleton()->input; - if (p_event_type == EMSCRIPTEN_EVENT_GAMEPADCONNECTED) { - - String guid = ""; - if (String::utf8(p_event->mapping) == "standard") - guid = "Default HTML5 Gamepad"; - input->joy_connection_changed(p_event->index, true, String::utf8(p_event->id), guid); - } else { - input->joy_connection_changed(p_event->index, false, ""); - } - return true; -} - -void OS_JavaScript::process_joypads() { - - int joypad_count = emscripten_get_num_gamepads(); - for (int joypad = 0; joypad < joypad_count; joypad++) { - EmscriptenGamepadEvent state; - EMSCRIPTEN_RESULT query_result = emscripten_get_gamepad_status(joypad, &state); - // Chromium reserves gamepads slots, so NO_DATA is an expected result. - ERR_CONTINUE(query_result != EMSCRIPTEN_RESULT_SUCCESS && - query_result != EMSCRIPTEN_RESULT_NO_DATA); - if (query_result == EMSCRIPTEN_RESULT_SUCCESS && state.connected) { - - int button_count = MIN(state.numButtons, 18); - int axis_count = MIN(state.numAxes, 8); - for (int button = 0; button < button_count; button++) { - - float value = state.analogButton[button]; - if (String::utf8(state.mapping) == "standard" && (button == JOY_ANALOG_L2 || button == JOY_ANALOG_R2)) { - InputDefault::JoyAxis joy_axis; - joy_axis.min = 0; - joy_axis.value = value; - input->joy_axis(joypad, button, joy_axis); - } else { - input->joy_button(joypad, button, value); - } - } - for (int axis = 0; axis < axis_count; axis++) { - - InputDefault::JoyAxis joy_axis; - joy_axis.min = -1; - joy_axis.value = state.axis[axis]; - input->joy_axis(joypad, axis, joy_axis); - } - } - } -} - -bool OS_JavaScript::is_joy_known(int p_device) { - - return input->is_joy_mapped(p_device); -} - -String OS_JavaScript::get_joy_guid(int p_device) const { - - return input->get_joy_guid_remapped(p_device); -} - -// Video - -int OS_JavaScript::get_video_driver_count() const { - - return VIDEO_DRIVER_MAX; -} - -const char *OS_JavaScript::get_video_driver_name(int p_driver) const { - - switch (p_driver) { - case VIDEO_DRIVER_GLES2: - return "GLES2"; - } - ERR_FAIL_V_MSG(nullptr, "Invalid video driver index: " + itos(p_driver) + "."); -} - -// Audio - -int OS_JavaScript::get_audio_driver_count() const { - - return 1; -} - -const char *OS_JavaScript::get_audio_driver_name(int p_driver) const { - - return "JavaScript"; -} - -// Clipboard -extern "C" EMSCRIPTEN_KEEPALIVE void update_clipboard(const char *p_text) { - // Only call set_clipboard from OS (sets local clipboard) - OS::get_singleton()->OS::set_clipboard(p_text); -} - -void OS_JavaScript::set_clipboard(const String &p_text) { - OS::set_clipboard(p_text); - /* clang-format off */ - int err = EM_ASM_INT({ - var text = UTF8ToString($0); - 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; - }, p_text.utf8().get_data()); - /* clang-format on */ - ERR_FAIL_COND_MSG(err, "Clipboard API is not supported."); -} - -String OS_JavaScript::get_clipboard() const { - /* clang-format off */ - EM_ASM({ - try { - navigator.clipboard.readText().then(function (result) { - ccall('update_clipboard', 'void', ['string'], [result]); - }).catch(function (e) { - // Fail graciously. - }); - } catch (e) { - // Fail graciously. - } - }); - /* clang-format on */ - return this->OS::get_clipboard(); -} +#include "godot_js.h" // Lifecycle -int OS_JavaScript::get_current_video_driver() const { - return video_driver_index; -} - -void OS_JavaScript::initialize_core() { - +void OS_JavaScript::initialize() { OS_Unix::initialize_core(); - FileAccess::make_default<FileAccessBufferedFA<FileAccessUnix>>(FileAccess::ACCESS_RESOURCES); -} - -Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { - -#if 0 - EmscriptenWebGLContextAttributes attributes; - emscripten_webgl_init_context_attributes(&attributes); - attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed"); - attributes.antialias = false; - ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER); - - if (p_desired.layered) { - set_window_per_pixel_transparency_enabled(true); - } + DisplayServerJavaScript::register_javascript_driver(); - bool gl_initialization_error = false; - - if (RasterizerGLES2::is_viable() == OK) { - attributes.majorVersion = 1; - RasterizerGLES2::register_config(); - RasterizerGLES2::make_current(); - } else { - gl_initialization_error = true; - } - - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(GODOT_CANVAS_SELECTOR, &attributes); - if (emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS) { - gl_initialization_error = true; - } - - if (gl_initialization_error) { - OS::get_singleton()->alert("Your browser does not seem to support WebGL. Please update your browser version.", - "Unable to initialize video driver"); - return ERR_UNAVAILABLE; - } - - video_driver_index = p_video_driver; - - video_mode = p_desired; - // fullscreen_change_callback will correct this if the request is successful. - video_mode.fullscreen = false; - // Emscripten only attempts fullscreen requests if the user input callback - // was registered through one its own functions, so request manually for - // start-up fullscreen. - if (p_desired.fullscreen) { - /* clang-format off */ - EM_ASM({ - const canvas = Module.canvas; - (canvas.requestFullscreen || canvas.msRequestFullscreen || - canvas.mozRequestFullScreen || canvas.mozRequestFullscreen || - canvas.webkitRequestFullscreen - ).call(canvas); - }); - /* clang-format on */ - } - /* clang-format off */ - if (EM_ASM_INT_V({ return Module.resizeCanvasOnStart })) { - /* clang-format on */ - set_window_size(Size2(video_mode.width, video_mode.height)); - } else { - set_window_size(get_window_size()); - } +#ifdef MODULE_WEBSOCKET_ENABLED + EngineDebugger::register_uri_handler("ws://", RemoteDebuggerPeerWebSocket::create); + EngineDebugger::register_uri_handler("wss://", RemoteDebuggerPeerWebSocket::create); #endif - RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? - - char locale_ptr[16]; - /* clang-format off */ - EM_ASM_ARGS({ - stringToUTF8(Module.locale, $0, 16); - }, locale_ptr); - /* clang-format on */ - setenv("LANG", locale_ptr, true); - - AudioDriverManager::initialize(p_audio_driver); - RenderingServer *rendering_server = memnew(RenderingServerRaster()); - input = memnew(InputDefault); - - EMSCRIPTEN_RESULT result; -#define EM_CHECK(ev) \ - if (result != EMSCRIPTEN_RESULT_SUCCESS) \ - ERR_PRINT("Error while setting " #ev " callback: Code " + itos(result)); -#define SET_EM_CALLBACK(target, ev, cb) \ - result = emscripten_set_##ev##_callback(target, nullptr, true, &cb); \ - EM_CHECK(ev) -#define SET_EM_CALLBACK_NOTARGET(ev, cb) \ - result = emscripten_set_##ev##_callback(nullptr, true, &cb); \ - EM_CHECK(ev) - // These callbacks from Emscripten's html5.h suffice to access most - // JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM - // is used below. - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, mousedown, mouse_button_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, wheel, wheel_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchstart, touch_press_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchmove, touchmove_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchend, touch_press_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchcancel, touch_press_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keydown, keydown_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keypress, keypress_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keyup, keyup_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback) - SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback) - SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback) -#undef SET_EM_CALLBACK_NOTARGET -#undef SET_EM_CALLBACK -#undef EM_CHECK - - /* clang-format off */ - EM_ASM_ARGS({ - const send_notification = cwrap('send_notification', null, ['number']); - const notifications = arguments; - (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { - Module.canvas.addEventListener(event, send_notification.bind(null, notifications[index])); - }); - // Clipboard - const update_clipboard = cwrap('update_clipboard', null, ['string']); - window.addEventListener('paste', function(evt) { - update_clipboard(evt.clipboardData.getData('text')); - }, true); - }, - NOTIFICATION_WM_MOUSE_ENTER, - NOTIFICATION_WM_MOUSE_EXIT, - NOTIFICATION_WM_FOCUS_IN, - NOTIFICATION_WM_FOCUS_OUT - ); - /* clang-format on */ - - rendering_server->init(); +} - return OK; +void OS_JavaScript::resume_audio() { + if (audio_driver_javascript) { + audio_driver_javascript->resume(); + } } void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) { - main_loop = p_main_loop; - input->set_main_loop(p_main_loop); } MainLoop *OS_JavaScript::get_main_loop() const { - return main_loop; } -void OS_JavaScript::run_async() { - - main_loop->init(); - emscripten_set_main_loop(main_loop_callback, -1, false); -} - -void OS_JavaScript::main_loop_callback() { - - get_singleton()->main_loop_iterate(); +void OS_JavaScript::fs_sync_callback() { + get_singleton()->idb_is_syncing = false; } bool OS_JavaScript::main_loop_iterate() { - - if (is_userfs_persistent() && sync_wait_time >= 0) { - int64_t current_time = get_ticks_msec(); - int64_t elapsed_time = current_time - last_sync_check_time; - last_sync_check_time = current_time; - - sync_wait_time -= elapsed_time; - - if (sync_wait_time < 0) { - /* clang-format off */ - EM_ASM( - FS.syncfs(function(error) { - if (error) { err('Failed to save IDB file system: ' + error.message); } - }); - ); - /* clang-format on */ - } + if (is_userfs_persistent() && idb_needs_sync && !idb_is_syncing) { + idb_is_syncing = true; + idb_needs_sync = false; + godot_js_os_fs_sync(&fs_sync_callback); } - if (emscripten_sample_gamepad_data() == EMSCRIPTEN_RESULT_SUCCESS) - process_joypads(); - - if (just_exited_fullscreen) { - if (window_maximized) { - EmscriptenFullscreenStrategy strategy; - strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; - strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; - strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = nullptr; - emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy); - } else { - emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, windowed_size.width, windowed_size.height); - } - just_exited_fullscreen = false; - } - - int canvas[2]; - emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, canvas, canvas + 1); - video_mode.width = canvas[0]; - video_mode.height = canvas[1]; - if (!window_maximized && !video_mode.fullscreen && !just_exited_fullscreen && !entering_fullscreen) { - windowed_size.width = canvas[0]; - windowed_size.height = canvas[1]; - } + DisplayServer::get_singleton()->process_events(); return Main::iteration(); } void OS_JavaScript::delete_main_loop() { - - memdelete(main_loop); + if (main_loop) { + memdelete(main_loop); + } + main_loop = nullptr; } void OS_JavaScript::finalize() { - - memdelete(input); + delete_main_loop(); + if (audio_driver_javascript) { + memdelete(audio_driver_javascript); + audio_driver_javascript = nullptr; + } } // Miscellaneous -Error OS_JavaScript::execute(const String &p_path, const List<String> &p_arguments, bool p_blocking, ProcessID *r_child_id, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex) { +Error OS_JavaScript::execute(const String &p_path, const List<String> &p_arguments, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex) { + return create_process(p_path, p_arguments); +} - ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "OS::execute() is not available on the HTML5 platform."); +Error OS_JavaScript::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id) { + Array args; + for (const List<String>::Element *E = p_arguments.front(); E; E = E->next()) { + args.push_back(E->get()); + } + String json_args = JSON::print(args); + int failed = godot_js_os_execute(json_args.utf8().get_data()); + ERR_FAIL_COND_V_MSG(failed, ERR_UNAVAILABLE, "OS::execute() or create_process() must be implemented in JavaScript via 'engine.setOnExecute' if required."); + return OK; } Error OS_JavaScript::kill(const ProcessID &p_pid) { - ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "OS::kill() is not available on the HTML5 platform."); } int OS_JavaScript::get_process_id() const { - ERR_FAIL_V_MSG(0, "OS::get_process_id() is not available on the HTML5 platform."); } -extern "C" EMSCRIPTEN_KEEPALIVE void send_notification(int p_notification) { - - if (p_notification == NOTIFICATION_WM_MOUSE_ENTER || p_notification == NOTIFICATION_WM_MOUSE_EXIT) { - cursor_inside_canvas = p_notification == NOTIFICATION_WM_MOUSE_ENTER; - } - OS_JavaScript::get_singleton()->get_main_loop()->notification(p_notification); -} - bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) { - - if (p_feature == "HTML5" || p_feature == "web") + if (p_feature == "HTML5" || p_feature == "web") { return true; + } #ifdef JAVASCRIPT_EVAL_ENABLED - if (p_feature == "JavaScript") + if (p_feature == "JavaScript") { return true; + } #endif - - return false; -} - -void OS_JavaScript::alert(const String &p_alert, const String &p_title) { - - /* clang-format off */ - EM_ASM_({ - window.alert(UTF8ToString($0)); - }, p_alert.utf8().get_data()); - /* clang-format on */ -} - -void OS_JavaScript::set_window_title(const String &p_title) { - - /* clang-format off */ - EM_ASM_({ - document.title = UTF8ToString($0); - }, p_title.utf8().get_data()); - /* clang-format on */ -} - -void OS_JavaScript::set_icon(const Ref<Image> &p_icon) { - - ERR_FAIL_COND(p_icon.is_null()); - Ref<Image> icon = p_icon; - if (icon->is_compressed()) { - icon = icon->duplicate(); - ERR_FAIL_COND(icon->decompress() != OK); +#ifndef NO_THREADS + if (p_feature == "threads") { + return true; } - if (icon->get_format() != Image::FORMAT_RGBA8) { - if (icon == p_icon) - icon = icon->duplicate(); - icon->convert(Image::FORMAT_RGBA8); +#endif +#if WASM_GDNATIVE + if (p_feature == "wasm32") { + return true; } +#endif - png_image png_meta; - memset(&png_meta, 0, sizeof png_meta); - png_meta.version = PNG_IMAGE_VERSION; - png_meta.width = icon->get_width(); - png_meta.height = icon->get_height(); - png_meta.format = PNG_FORMAT_RGBA; - - PackedByteArray png; - size_t len; - PackedByteArray data = icon->get_data(); - ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); - - png.resize(len); - ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); - - /* clang-format off */ - EM_ASM_ARGS({ - var PNG_PTR = $0; - var PNG_LEN = $1; - - var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: "image/png" }); - var url = URL.createObjectURL(png); - var 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); - } - link.href = url; - }, png.ptr(), len); - /* clang-format on */ + return false; } String OS_JavaScript::get_executable_path() const { - return OS::get_executable_path(); } Error OS_JavaScript::shell_open(String p_uri) { - // Open URI in a new tab, browser will deal with it by protocol. - /* clang-format off */ - EM_ASM_({ - window.open(UTF8ToString($0), '_blank'); - }, p_uri.utf8().get_data()); - /* clang-format on */ + godot_js_os_shell_open(p_uri.utf8().get_data()); return OK; } String OS_JavaScript::get_name() const { - return "HTML5"; } -bool OS_JavaScript::can_draw() const { - - return true; // Always? -} - String OS_JavaScript::get_user_data_dir() const { - return "/userfs"; }; -String OS_JavaScript::get_resource_dir() const { - - return "/"; +String OS_JavaScript::get_cache_path() const { + return "/home/web_user/.cache"; } -void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) { - - OS_JavaScript *os = get_singleton(); - if (os->is_userfs_persistent() && p_file.begins_with("/userfs") && p_flags & FileAccess::WRITE) { - os->last_sync_check_time = OS::get_singleton()->get_ticks_msec(); - // Wait five seconds in case more files are about to be closed. - os->sync_wait_time = 5000; - } +String OS_JavaScript::get_config_path() const { + return "/home/web_user/.config"; } -void OS_JavaScript::set_idb_available(bool p_idb_available) { +String OS_JavaScript::get_data_path() const { + return "/home/web_user/.local/share"; +} - idb_available = p_idb_available; +void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) { + OS_JavaScript *os = OS_JavaScript::get_singleton(); + if (!(os->is_userfs_persistent() && (p_flags & FileAccess::WRITE))) { + return; // FS persistence is not working or we are not writing. + } + bool is_file_persistent = p_file.begins_with("/userfs"); +#ifdef TOOLS_ENABLED + // Hack for editor persistence (can we track). + is_file_persistent = is_file_persistent || p_file.begins_with("/home/web_user/"); +#endif + if (is_file_persistent) { + os->idb_needs_sync = true; + } } bool OS_JavaScript::is_userfs_persistent() const { - return idb_available; } -OS_JavaScript *OS_JavaScript::get_singleton() { +Error OS_JavaScript::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { + String path = p_path.get_file(); + p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); + ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ". Error: " + dlerror()); + return OK; +} +OS_JavaScript *OS_JavaScript::get_singleton() { return static_cast<OS_JavaScript *>(OS::get_singleton()); } -OS_JavaScript::OS_JavaScript(int p_argc, char *p_argv[]) { - - List<String> arguments; - for (int i = 1; i < p_argc; i++) { - arguments.push_back(String::utf8(p_argv[i])); - } - set_cmdline(p_argv[0], arguments); - - last_click_button_index = -1; - last_click_ms = 0; - last_click_pos = Point2(-100, -100); - - window_maximized = false; - entering_fullscreen = false; - just_exited_fullscreen = false; - transparency_enabled = false; +void OS_JavaScript::initialize_joypads() { +} - main_loop = nullptr; +OS_JavaScript::OS_JavaScript() { + char locale_ptr[16]; + godot_js_config_locale_get(locale_ptr, 16); + setenv("LANG", locale_ptr, true); - idb_available = false; - sync_wait_time = -1; + if (AudioDriverJavaScript::is_available()) { + audio_driver_javascript = memnew(AudioDriverJavaScript); + AudioDriverManager::add_driver(audio_driver_javascript); + } - AudioDriverManager::add_driver(&audio_driver_javascript); + idb_available = godot_js_os_fs_is_persistent(); Vector<Logger *> loggers; loggers.push_back(memnew(StdLogger)); diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index feb0f0651a..8db62d9d1c 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -35,135 +35,64 @@ #include "core/input/input.h" #include "drivers/unix/os_unix.h" #include "servers/audio_server.h" -#include "servers/rendering/rasterizer.h" #include <emscripten/html5.h> class OS_JavaScript : public OS_Unix { + MainLoop *main_loop = nullptr; + AudioDriverJavaScript *audio_driver_javascript = nullptr; - VideoMode video_mode; - Vector2 windowed_size; - bool window_maximized; - bool entering_fullscreen; - bool just_exited_fullscreen; - bool transparency_enabled; - - InputDefault *input; - Ref<InputEventKey> deferred_key_event; - CursorShape cursor_shape; - String cursors[CURSOR_MAX]; - Map<CursorShape, Vector<Variant>> cursors_cache; - Point2 touches[32]; - - Point2i last_click_pos; - double last_click_ms; - int last_click_button_index; - - MainLoop *main_loop; - int video_driver_index; - AudioDriverJavaScript audio_driver_javascript; - - bool idb_available; - int64_t sync_wait_time; - int64_t last_sync_check_time; - - static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data); - - static EM_BOOL keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); - static EM_BOOL keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); - static EM_BOOL keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); - - static EM_BOOL mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); - static EM_BOOL mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); - - static EM_BOOL wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data); - - static EM_BOOL touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); - static EM_BOOL touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); - - static EM_BOOL gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data); - void process_joypads(); + bool idb_is_syncing = false; + bool idb_available = false; + bool idb_needs_sync = false; static void main_loop_callback(); static void file_access_close_callback(const String &p_file, int p_flags); + static void fs_sync_callback(); protected: - virtual int get_current_video_driver() const; + void initialize() override; - virtual void initialize_core(); - virtual Error initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver); + void set_main_loop(MainLoop *p_main_loop) override; + void delete_main_loop() override; - virtual void set_main_loop(MainLoop *p_main_loop); - virtual void delete_main_loop(); + void finalize() override; - virtual void finalize(); - - virtual bool _check_internal_feature_support(const String &p_feature); + bool _check_internal_feature_support(const String &p_feature) override; public: // Override return type to make writing static callbacks less tedious. static OS_JavaScript *get_singleton(); - virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0); - virtual VideoMode get_video_mode(int p_screen = 0) const; - virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const; - - virtual void set_window_size(const Size2); - virtual Size2 get_window_size() const; - virtual void set_window_maximized(bool p_enabled); - virtual bool is_window_maximized() const; - virtual void set_window_fullscreen(bool p_enabled); - virtual bool is_window_fullscreen() const; - virtual Size2 get_screen_size(int p_screen = -1) const; - - virtual Point2 get_mouse_position() const; - virtual int get_mouse_button_state() const; - virtual void set_cursor_shape(CursorShape p_shape); - virtual void set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot); - virtual void set_mouse_mode(MouseMode p_mode); - virtual MouseMode get_mouse_mode() const; - - virtual bool get_window_per_pixel_transparency_enabled() const; - virtual void set_window_per_pixel_transparency_enabled(bool p_enabled); - - virtual bool has_touchscreen_ui_hint() const; + void initialize_joypads() override; - virtual bool is_joy_known(int p_device); - virtual String get_joy_guid(int p_device) const; - - virtual int get_video_driver_count() const; - virtual const char *get_video_driver_name(int p_driver) const; - - virtual int get_audio_driver_count() const; - virtual const char *get_audio_driver_name(int p_driver) const; - - virtual void set_clipboard(const String &p_text); - virtual String get_clipboard() const; - - virtual MainLoop *get_main_loop() const; - void run_async(); + MainLoop *get_main_loop() const override; bool main_loop_iterate(); - virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr); - virtual Error kill(const ProcessID &p_pid); - virtual int get_process_id() const; + Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr) override; + Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override; + Error kill(const ProcessID &p_pid) override; + int get_process_id() const override; + + String get_executable_path() const override; + Error shell_open(String p_uri) override; + String get_name() const override; + // Override default OS implementation which would block the main thread with delay_usec. + // Implemented in javascript_main.cpp loop callback instead. + void add_frame_delay(bool p_can_draw) override {} - virtual void alert(const String &p_alert, const String &p_title = "ALERT!"); - virtual void set_window_title(const String &p_title); - virtual void set_icon(const Ref<Image> &p_icon); - String get_executable_path() const; - virtual Error shell_open(String p_uri); - virtual String get_name() const; - virtual bool can_draw() const; + String get_cache_path() const override; + String get_config_path() const override; + String get_data_path() const override; + String get_user_data_dir() const override; - virtual String get_resource_dir() const; - virtual String get_user_data_dir() const; + bool is_userfs_persistent() const override; + Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) override; - void set_idb_available(bool p_idb_available); - virtual bool is_userfs_persistent() const; + void resume_audio(); - OS_JavaScript(int p_argc, char *p_argv[]); + OS_JavaScript(); }; #endif diff --git a/platform/javascript/package-lock.json b/platform/javascript/package-lock.json new file mode 100644 index 0000000000..8e298a495e --- /dev/null +++ b/platform/javascript/package-lock.json @@ -0,0 +1,1605 @@ +{ + "name": "godot", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@eslint/eslintrc": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", + "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, + "ajv": { + "version": "6.12.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", + "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "confusing-browser-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", + "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.18.0-next.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", + "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.9.0.tgz", + "integrity": "sha512-V6QyhX21+uXp4T+3nrNfI3hQNBDa/P8ga7LoQOenwrlEFXrEnUEE+ok1dMtaS3b6rmLXhT1TkTIsG75HMLbknA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.1.3", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.0", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.3.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-config-airbnb-base": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.0.tgz", + "integrity": "sha512-Snswd5oC6nJaevs3nZoLSTvGJBvzTfnBqOIArkf3cbyTyq9UD79wOk8s+RiL6bhca0p/eRO6veczhf6A/7Jy8Q==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.9", + "object.assign": "^4.1.0", + "object.entries": "^1.1.2" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz", + "integrity": "sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.3", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-callable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz", + "integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + } + } +} diff --git a/platform/javascript/package.json b/platform/javascript/package.json new file mode 100644 index 0000000000..630b584f5b --- /dev/null +++ b/platform/javascript/package.json @@ -0,0 +1,24 @@ +{ + "name": "godot", + "private": true, + "version": "1.0.0", + "description": "Linting setup for Godot's HTML5 platform code", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "npm run lint:engine && npm run lint:libs && npm run lint:modules", + "lint:engine": "eslint \"js/engine/*.js\" --no-eslintrc -c .eslintrc.engine.js", + "lint:libs": "eslint \"js/libs/*.js\" --no-eslintrc -c .eslintrc.libs.js", + "lint:modules": "eslint \"../../modules/**/*.js\" --no-eslintrc -c .eslintrc.libs.js", + "format": "npm run format:engine && npm run format:libs && npm run format:modules", + "format:engine": "npm run lint:engine -- --fix", + "format:libs": "npm run lint:libs -- --fix", + "format:modules": "npm run lint:modules -- --fix" + }, + "author": "Godot Engine contributors", + "license": "MIT", + "devDependencies": { + "eslint": "^7.9.0", + "eslint-config-airbnb-base": "^14.2.0", + "eslint-plugin-import": "^2.22.0" + } +} diff --git a/platform/javascript/platform_config.h b/platform/javascript/platform_config.h index e2200376d3..65df34902e 100644 --- a/platform/javascript/platform_config.h +++ b/platform/javascript/platform_config.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ |