diff options
Diffstat (limited to 'platform/javascript')
47 files changed, 4946 insertions, 1580 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 a8861124b9..11a45d2811 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -9,36 +9,74 @@ javascript_files = [      "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 = [ -    "native/http_request.js", -    "native/library_godot_audio.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_pre = [ -    "native/id_handler.js", -    "native/utils.js", -] -for js in js_pre: -    env.Append(LINKFLAGS=["--pre-js", env.File(js).path]) -env.Depends(build, js_pre) +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"]) +    # 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"]) + +    # 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/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) @@ -54,13 +92,29 @@ 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/full-size.html" -in_files = [js_wrapped, build[1], html_file] -if env["threads_enabled"]: -    in_files.append(build[2]) +if env["tools"]: +    subst_dict = {"\$GODOT_VERSION": env.GetBuildVersion()} +    html_file = env.Substfile( +        target="#bin/godot${PROGSUFFIX}.html", source="#misc/dist/html/editor.html", SUBST_DICT=subst_dict +    ) + +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(      "#bin/godot", diff --git a/platform/javascript/api/api.cpp b/platform/javascript/api/api.cpp index 9c73e5c4c4..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,12 +29,14 @@  /*************************************************************************/  #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)); 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 6ea948004e..f7cc9e6540 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,8 +30,7 @@  #include "audio_driver_javascript.h" -#include "core/project_settings.h" -#include "godot_audio.h" +#include "core/config/project_settings.h"  #include <emscripten.h> @@ -45,92 +44,109 @@ const char *AudioDriverJavaScript::get_name() const {  	return "JavaScript";  } -#ifndef NO_THREADS -void AudioDriverJavaScript::_audio_thread_func(void *p_data) { -	AudioDriverJavaScript *obj = static_cast<AudioDriverJavaScript *>(p_data); -	while (!obj->quit) { -		obj->lock(); -		if (!obj->needs_process) { -			obj->unlock(); -			OS::get_singleton()->delay_usec(1000); // Give the browser some slack. -			continue; -		} -		obj->_js_driver_process(); -		obj->needs_process = false; -		obj->unlock(); -	} +void AudioDriverJavaScript::_state_change_callback(int p_state) { +	singleton->state = p_state;  } -#endif -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_start() { -#ifndef NO_THREADS -	AudioDriverJavaScript::singleton->lock(); -#else -	AudioDriverJavaScript::singleton->_js_driver_process(); -#endif +void AudioDriverJavaScript::_latency_update_callback(float p_latency) { +	singleton->output_latency = p_latency;  } -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_end() { -#ifndef NO_THREADS -	AudioDriverJavaScript::singleton->needs_process = true; -	AudioDriverJavaScript::singleton->unlock(); -#endif -} +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); -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) { -	AudioDriverJavaScript::singleton->process_capture(sample); +	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::_js_driver_process() { -	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; +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); -void AudioDriverJavaScript::process_capture(float sample) { -	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() {  	mix_rate = GLOBAL_GET("audio/mix_rate");  	int latency = GLOBAL_GET("audio/output_latency"); -	channel_count = godot_audio_init(mix_rate, latency); -	buffer_length = closest_power_of_2((latency * mix_rate / 1000) * channel_count); -	buffer_length = godot_audio_create_processor(buffer_length, channel_count); -	if (!buffer_length) { -		return FAILED; +	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;  	} - -	if (!internal_buffer) { +	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() { -#ifndef NO_THREADS -	thread = Thread::create(_audio_thread_func, this); -#endif -	godot_audio_start(internal_buffer); +	if (node) { +		node->start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb)); +	}  }  void AudioDriverJavaScript::resume() { -	godot_audio_resume(); +	if (state == 0) { // 'suspended' +		godot_audio_resume(); +	}  }  float AudioDriverJavaScript::get_latency() { -	return godot_audio_get_latency(); +	return output_latency + (float(buffer_length) / mix_rate);  }  int AudioDriverJavaScript::get_mix_rate() const { @@ -142,48 +158,128 @@ AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const {  }  void AudioDriverJavaScript::lock() { -#ifndef NO_THREADS -	mutex.lock(); -#endif +	if (node) { +		node->unlock(); +	}  }  void AudioDriverJavaScript::unlock() { -#ifndef NO_THREADS -	mutex.unlock(); -#endif -} - -void AudioDriverJavaScript::finish_async() { -#ifndef NO_THREADS -	quit = true; // Ask thread to quit. -#endif -	godot_audio_finish_async(); +	if (node) { +		node->unlock(); +	}  }  void AudioDriverJavaScript::finish() { -#ifndef NO_THREADS -	Thread::wait_to_finish(thread); -	memdelete(thread); -	thread = NULL; -#endif -	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;  	}  }  Error AudioDriverJavaScript::capture_start() { -	godot_audio_capture_stop(); +	lock();  	input_buffer_init(buffer_length); -	godot_audio_capture_start(); +	unlock(); +	if (godot_audio_capture_start()) { +		return FAILED; +	}  	return OK;  }  Error AudioDriverJavaScript::capture_stop() { +	godot_audio_capture_stop(); +	lock();  	input_buffer.clear(); +	unlock();  	return OK;  }  AudioDriverJavaScript::AudioDriverJavaScript() {  	singleton = this;  } + +#ifdef NO_THREADS +/// ScriptProcessorNode implementation +void AudioDriverJavaScript::ScriptProcessorNode::_process_callback() { +	AudioDriverJavaScript::singleton->_audio_driver_capture(); +	AudioDriverJavaScript::singleton->_audio_driver_process(); +} + +int AudioDriverJavaScript::ScriptProcessorNode::create(int p_buffer_samples, int p_channels) { +	return godot_audio_script_create(p_buffer_samples, p_channels); +} + +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; +			} +		} +		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); +	} +} + +int AudioDriverJavaScript::WorkletNode::create(int p_buffer_size, int p_channels) { +	godot_audio_worklet_create(p_channels); +	return p_buffer_size; +} + +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.start(_audio_thread_func, this); +} + +void AudioDriverJavaScript::WorkletNode::lock() { +	mutex.lock(); +} + +void AudioDriverJavaScript::WorkletNode::unlock() { +	mutex.unlock(); +} + +void AudioDriverJavaScript::WorkletNode::finish() { +	quit = true; // Ask thread to quit. +	thread.wait_to_finish(); +} +#endif diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index 56a7da0307..393693640f 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,53 +31,96 @@  #ifndef AUDIO_DRIVER_JAVASCRIPT_H  #define AUDIO_DRIVER_JAVASCRIPT_H -#include "servers/audio_server.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; +		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(); + +	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; +	}; +  private: -	float *internal_buffer = nullptr; +	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; -public: -#ifndef NO_THREADS -	Mutex mutex; -	Thread *thread = nullptr; -	bool quit = false; -	bool needs_process = true; - -	static void _audio_thread_func(void *p_data); -#endif +	static void _state_change_callback(int p_state); +	static void _latency_update_callback(float p_latency); -	void _js_driver_process(); +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:  	static bool is_available(); -	void process_capture(float sample);  	static AudioDriverJavaScript *singleton; -	const char *get_name() const override; +	virtual const char *get_name() const; -	Error init() override; -	void start() override; +	virtual Error init(); +	virtual void start();  	void resume(); -	float get_latency() override; -	int get_mix_rate() const override; -	SpeakerMode get_speaker_mode() const override; -	void lock() override; -	void unlock() override; -	void finish() override; -	void finish_async(); +	virtual float get_latency(); +	virtual int get_mix_rate() const; +	virtual SpeakerMode get_speaker_mode() const; +	virtual void lock(); +	virtual void unlock(); +	virtual void finish(); -	Error capture_start() override; -	Error capture_stop() override; +	virtual Error capture_start(); +	virtual Error capture_stop();  	AudioDriverJavaScript();  }; -  #endif diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 4b5890545f..653d18f791 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -1,6 +1,15 @@  import os - -from emscripten_helpers import run_closure_compiler, create_engine_file +import sys + +from emscripten_helpers import ( +    run_closure_compiler, +    create_engine_file, +    add_js_libraries, +    add_js_pre, +    add_js_externs, +    get_build_version, +) +from methods import get_compiler_version  from SCons.Util import WhereIs @@ -20,9 +29,17 @@ 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("gdnative_enabled", "Enable WebAssembly GDNative support (produces bigger binaries)", False),          BoolVariable("use_closure_compiler", "Use closure compiler to minimize JavaScript code", False),      ] @@ -40,9 +57,13 @@ def get_flags():  def configure(env): +    try: +        env["initial_memory"] = int(env["initial_memory"]) +    except Exception: +        print("Initial memory must be a valid integer") +        sys.exit(255)      ## Build type -      if env["target"] == "release":          # Use -Os to prioritize optimizing for reduced file size. This is          # particularly valuable for the web platform because it directly @@ -62,15 +83,20 @@ def configure(env):          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. @@ -78,14 +104,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"]: @@ -95,6 +139,17 @@ 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 for getting build version string. +    env.AddMethod(get_build_version, "GetBuildVersion") +      # Add method that joins/compiles our Engine files.      env.AddMethod(create_engine_file, "CreateEngineFile") @@ -103,7 +158,6 @@ def configure(env):      env["CC"] = "emcc"      env["CXX"] = "em++" -    env["LINK"] = "emcc"      env["AR"] = "emar"      env["RANLIB"] = "emranlib" @@ -130,25 +184,34 @@ 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"]) +    # 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 @@ -159,12 +222,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"])      # Allow use to take control of swapping WebGL buffers.      env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"]) -    # callMain for manual start, FS for preloading, PATH and ERRNO_CODES for BrowserFS. -    env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain', 'FS', 'PATH']"]) +    # 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 index 8dc33bdf64..cfe093693f 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_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       */ @@ -37,6 +37,7 @@  #include <png.h>  #include "dom_keys.inc" +#include "godot_js.h"  #define DOM_BUTTON_LEFT 0  #define DOM_BUTTON_MIDDLE 1 @@ -44,28 +45,17 @@  #define DOM_BUTTON_XBUTTON1 3  #define DOM_BUTTON_XBUTTON2 4 -char DisplayServerJavaScript::canvas_id[256] = { 0 }; -static bool cursor_inside_canvas = true; -  DisplayServerJavaScript *DisplayServerJavaScript::get_singleton() {  	return static_cast<DisplayServerJavaScript *>(DisplayServer::get_singleton());  }  // Window (canvas)  void DisplayServerJavaScript::focus_canvas() { -	/* clang-format off */ -	EM_ASM( -		Module['canvas'].focus(); -	); -	/* clang-format on */ +	godot_js_display_canvas_focus();  }  bool DisplayServerJavaScript::is_canvas_focused() { -	/* clang-format off */ -	return EM_ASM_INT_V( -		return document.activeElement == Module['canvas']; -	); -	/* clang-format on */ +	return godot_js_display_canvas_is_focused() != 0;  }  bool DisplayServerJavaScript::check_size_force_redraw() { @@ -83,19 +73,17 @@ bool DisplayServerJavaScript::check_size_force_redraw() {  }  Point2 DisplayServerJavaScript::compute_position_in_canvas(int p_x, int p_y) { -	int canvas_x = EM_ASM_INT({ -		return Module['canvas'].getBoundingClientRect().x; -	}); -	int canvas_y = EM_ASM_INT({ -		return Module['canvas'].getBoundingClientRect().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(canvas_id, &canvas_width, &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(canvas_id, &element_width, &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))); @@ -105,8 +93,7 @@ EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, co  	DisplayServerJavaScript *display = get_singleton();  	// Empty ID is canvas.  	String target_id = String::utf8(p_event->id); -	String canvas_str_id = String::utf8(canvas_id); -	if (target_id.empty() || target_id == canvas_str_id) { +	if (target_id.is_empty() || target_id == String::utf8(&(display->canvas_id[1]))) {  		// This event property is the only reliable data on  		// browser fullscreen state.  		if (p_event->isFullscreen) { @@ -118,14 +105,15 @@ EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, co  	return false;  } -// Drag and drop callback (see native/utils.js). -extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p_filec) { -	DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); +// 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()) +	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])); @@ -137,6 +125,18 @@ extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p  	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> @@ -272,12 +272,13 @@ EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const E  }  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 (!cursor_inside_canvas && !input_mask) +	if (!ds->cursor_inside_canvas && !input_mask)  		return false;  	Ref<InputEventMouseMotion> ev; @@ -339,35 +340,13 @@ const char *DisplayServerJavaScript::godot2dom_cursor(DisplayServer::CursorShape  	}  } -void DisplayServerJavaScript::set_css_cursor(const char *p_cursor) { -	/* clang-format off */ -	EM_ASM_({ -		Module['canvas'].style.cursor = UTF8ToString($0); -	}, p_cursor); -	/* clang-format on */ -} - -bool DisplayServerJavaScript::is_css_cursor_hidden() const { -	/* clang-format off */ -	return EM_ASM_INT({ -		return Module['canvas'].style.cursor === 'none'; -	}); -	/* clang-format on */ -} -  void DisplayServerJavaScript::cursor_set_shape(CursorShape p_shape) {  	ERR_FAIL_INDEX(p_shape, CURSOR_MAX); - -	if (mouse_get_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)); -		} +	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 { @@ -376,17 +355,6 @@ DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const {  void DisplayServerJavaScript::cursor_set_custom_image(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) { -				cursor_set_shape(p_shape); -				return; -			} - -			cursors_cache.erase(p_shape); -		} -  		Ref<Texture2D> texture = p_cursor;  		Ref<AtlasTexture> atlas_texture = p_cursor;  		Ref<Image> image; @@ -449,53 +417,10 @@ void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, Curso  		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; +		godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), png.ptr(), len, p_hotspot.x, p_hotspot.y); -		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); +	} else { +		godot_js_display_cursor_set_custom_shape(godot2dom_cursor(p_shape), NULL, 0, 0, 0);  	}  	cursor_set_shape(cursor_shape); @@ -508,40 +433,37 @@ void DisplayServerJavaScript::mouse_set_mode(MouseMode p_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)); -		cursor_set_shape(cursor_shape); +		godot_js_display_cursor_set_visible(1);  		emscripten_exit_pointerlock();  	} else if (p_mode == MOUSE_MODE_HIDDEN) { -		set_css_cursor("none"); +		godot_js_display_cursor_set_visible(0);  		emscripten_exit_pointerlock();  	} else if (p_mode == MOUSE_MODE_CAPTURED) { -		EMSCRIPTEN_RESULT result = emscripten_request_pointerlock("canvas", false); +		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."); -		// set_css_cursor must be called before cursor_set_shape to make the cursor visible -		set_css_cursor(godot2dom_cursor(cursor_shape)); -		cursor_set_shape(cursor_shape);  	}  }  DisplayServer::MouseMode DisplayServerJavaScript::mouse_get_mode() const { -	if (is_css_cursor_hidden()) +	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) == "canvas") ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; +	return (ev.isActive && String::utf8(ev.id) == String::utf8(&canvas_id[1])) ? 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 (cursor_inside_canvas) { +		if (ds->cursor_inside_canvas) {  			focus_canvas();  		} else {  			return false; @@ -632,63 +554,55 @@ EM_BOOL DisplayServerJavaScript::touchmove_callback(int p_event_type, const Emsc  }  bool DisplayServerJavaScript::screen_is_touchscreen(int p_screen) const { -	return EM_ASM_INT({ return 'ontouchstart' in window; }); +	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) { +void DisplayServerJavaScript::gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid) {  	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); +	if (p_connected) { +		input->joy_connection_changed(p_index, true, String::utf8(p_id), String::utf8(p_guid));  	} else { -		input->joy_connection_changed(p_event->index, false, ""); +		input->joy_connection_changed(p_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++) { +	int32_t pads = godot_js_display_gamepad_sample_count(); +	int32_t s_btns_num = 0; +	int32_t s_axes_num = 0; +	int32_t s_standard = 0; +	float s_btns[16]; +	float s_axes[10]; +	for (int idx = 0; idx < pads; idx++) { +		int err = godot_js_display_gamepad_sample_get(idx, s_btns, &s_btns_num, s_axes, &s_axes_num, &s_standard); +		if (err) { +			continue; +		} +		for (int b = 0; b < s_btns_num; b++) { +			float value = s_btns[b]; +			// Buttons 6 and 7 in the standard mapping need to be +			// axis to be handled as JOY_AXIS_TRIGGER by Godot. +			if (s_standard && (b == 6 || b == 7)) {  				Input::JoyAxis joy_axis; -				joy_axis.min = -1; -				joy_axis.value = state.axis[axis]; -				input->joy_axis(joypad, axis, joy_axis); +				joy_axis.min = 0; +				joy_axis.value = value; +				int a = b == 6 ? JOY_AXIS_TRIGGER_LEFT : JOY_AXIS_TRIGGER_RIGHT; +				input->joy_axis(idx, a, joy_axis); +			} else { +				input->joy_button(idx, b, value);  			}  		} +		for (int a = 0; a < s_axes_num; a++) { +			Input::JoyAxis joy_axis; +			joy_axis.min = -1; +			joy_axis.value = s_axes[a]; +			input->joy_axis(idx, a, 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"); @@ -696,53 +610,30 @@ Vector<String> DisplayServerJavaScript::get_rendering_drivers_func() {  }  // Clipboard -extern "C" EMSCRIPTEN_KEEPALIVE void update_clipboard(const char *p_text) { -	// Only call set_clipboard from OS (sets local clipboard) -	DisplayServerJavaScript::get_singleton()->clipboard = p_text; +void DisplayServerJavaScript::update_clipboard_callback(const char *p_text) { +	get_singleton()->clipboard = p_text;  }  void DisplayServerJavaScript::clipboard_set(const String &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 */ +	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 { -	/* 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 */ +	godot_js_display_clipboard_get(update_clipboard_callback);  	return clipboard;  } -extern "C" EMSCRIPTEN_KEEPALIVE void send_window_event(int p_notification) { +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) { -		cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER; +		ds->cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER;  	} -	OS_JavaScript *os = OS_JavaScript::get_singleton(); -	if (os->is_finalizing()) -		return; // We don't want events anymore. -	DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); -	if (ds && !ds->window_event_callback.is_null()) { +	if (!ds->window_event_callback.is_null()) {  		Variant event = int(p_notification);  		Variant *eventp = &event;  		Variant ret; @@ -752,11 +643,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE void send_window_event(int p_notification) {  }  void DisplayServerJavaScript::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 */ +	godot_js_display_alert(p_alert.utf8().get_data());  }  void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) { @@ -787,29 +674,11 @@ void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) {  	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({ -		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 */ +	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(); -	if (os->is_finalizing()) -		return; // We don't want events anymore.  	// Resume audio context after input in case autoplay was denied.  	os->resume_audio(); @@ -831,16 +700,14 @@ DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_dr  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. -	/* clang-format off */ -	swap_cancel_ok = EM_ASM_INT({ -		const win = (['Windows', 'Win64', 'Win32', 'WinCE']); -		const plat = navigator.platform || ""; -		if (win.indexOf(plat) !== -1) { -			return 1; -		} -		return 0; -	}) == 1; -	/* clang-format on */ +	// 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 @@ -878,10 +745,8 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive  	video_driver_index = p_video_driver;  #endif -	/* clang-format off */  	window_set_mode(p_mode); -	if (EM_ASM_INT_V({ return Module['resizeCanvasOnStart'] })) { -		/* clang-format on */ +	if (godot_js_config_is_resize_on_start()) {  		window_set_size(p_resolution);  	} @@ -895,12 +760,8 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive  #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. For APIs that are not (sufficiently) exposed, EM_ASM -	// is used below. +	// 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) @@ -913,48 +774,24 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive  	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 -	/* clang-format off */ -	EM_ASM_ARGS({ -		// Bind native event listeners. -		// Module.listeners, and Module.drop_handler are defined in native/utils.js -		const canvas = Module['canvas']; -		const send_window_event = cwrap('send_window_event', null, ['number']); -		const notifications = arguments; -		(['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { -			Module.listeners.add(canvas, event, send_window_event.bind(null, notifications[index]), true); -		}); -		// Clipboard -		const update_clipboard = cwrap('update_clipboard', null, ['string']); -		Module.listeners.add(window, 'paste', function(evt) { -			update_clipboard(evt.clipboardData.getData('text')); -		}, false); -		// Drag an drop -		Module.listeners.add(canvas, 'dragover', function(ev) { -			// Prevent default behavior (which would try to open the file(s)) -			ev.preventDefault(); -		}, false); -		Module.listeners.add(canvas, 'drop', Module.drop_handler, false); -	}, -		WINDOW_EVENT_MOUSE_ENTER, -		WINDOW_EVENT_MOUSE_EXIT, -		WINDOW_EVENT_FOCUS_IN, -		WINDOW_EVENT_FOCUS_OUT -	); -	/* clang-format on */ +	// 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); +	godot_js_display_gamepad_cb(&DisplayServerJavaScript::gamepad_callback);  	Input::get_singleton()->set_event_dispatch_function(_dispatch_input_event);  }  DisplayServerJavaScript::~DisplayServerJavaScript() { -	EM_ASM({ -		Module.listeners.clear(); -	});  	//emscripten_webgl_commit_frame();  	//emscripten_webgl_destroy_context(webgl_ctx);  } @@ -1057,11 +894,7 @@ void DisplayServerJavaScript::window_set_drop_files_callback(const Callable &p_c  }  void DisplayServerJavaScript::window_set_title(const String &p_title, WindowID p_window) { -	/* clang-format off */ -	EM_ASM_({ -		document.title = UTF8ToString($0); -	}, p_title.utf8().get_data()); -	/* clang-format on */ +	godot_js_display_window_title_set(p_title.utf8().get_data());  }  int DisplayServerJavaScript::window_get_current_screen(WindowID p_window) const { @@ -1103,11 +936,9 @@ Size2i DisplayServerJavaScript::window_get_min_size(WindowID p_window) const {  void DisplayServerJavaScript::window_set_size(const Size2i p_size, WindowID p_window) {  	last_width = p_size.x;  	last_height = p_size.y; -	double scale = EM_ASM_DOUBLE({ -		return window.devicePixelRatio || 1; -	}); -	emscripten_set_canvas_element_size(canvas_id, p_size.x * scale, p_size.y * scale); -	emscripten_set_element_css_size(canvas_id, p_size.x, 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 { @@ -1130,7 +961,7 @@ void DisplayServerJavaScript::window_set_mode(WindowMode p_mode, WindowID p_wind  				emscripten_exit_fullscreen();  			}  			window_mode = WINDOW_MODE_WINDOWED; -			window_set_size(windowed_size); +			window_set_size(Size2i(last_width, last_height));  		} break;  		case WINDOW_MODE_FULLSCREEN: {  			EmscriptenFullscreenStrategy strategy; @@ -1184,8 +1015,9 @@ bool DisplayServerJavaScript::can_any_window_draw() const {  }  void DisplayServerJavaScript::process_events() { -	if (emscripten_sample_gamepad_data() == EMSCRIPTEN_RESULT_SUCCESS) +	if (godot_js_display_gamepad_sample() == OK) {  		process_joypads(); +	}  }  int DisplayServerJavaScript::get_current_video_driver() const { diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index d7116be36f..e28fbc56f3 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_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       */ @@ -37,18 +37,22 @@  #include <emscripten/html5.h>  class DisplayServerJavaScript : public DisplayServer { -	//int video_driver_index; - -	Vector2 windowed_size; - +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; -	CursorShape cursor_shape = CURSOR_ARROW; -	String cursors[CURSOR_MAX]; -	Map<CursorShape, Vector<Variant>> cursors_cache;  	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; @@ -66,8 +70,6 @@ class DisplayServerJavaScript : public DisplayServer {  	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); -	static void set_css_cursor(const char *p_cursor); -	bool is_css_cursor_hidden() const;  	// events  	static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data); @@ -84,7 +86,7 @@ class DisplayServerJavaScript : public DisplayServer {  	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); +	static void gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid);  	void process_joypads();  	static Vector<String> get_rendering_drivers_func(); @@ -92,22 +94,17 @@ class DisplayServerJavaScript : public DisplayServer {  	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(); -	static char canvas_id[256]; - -	WindowMode window_mode = WINDOW_MODE_WINDOWED; - -	String clipboard; - -	Callable window_event_callback; -	Callable input_event_callback; -	Callable input_text_callback; -	Callable drop_files_callback;  	// utilities  	bool check_size_force_redraw(); diff --git a/platform/javascript/dom_keys.inc b/platform/javascript/dom_keys.inc index e3f2ce42b4..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       */ diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py index f6db10fbbd..d08555916b 100644 --- a/platform/javascript/emscripten_helpers.py +++ b/platform/javascript/emscripten_helpers.py @@ -15,7 +15,28 @@ def run_closure_compiler(target, source, env, for_signature):      return " ".join(cmd) +def get_build_version(env): +    import version + +    name = "custom_build" +    if os.getenv("BUILD_NAME") != None: +        name = os.getenv("BUILD_NAME") +    return "%d.%d.%d.%s.%s" % (version.major, version.minor, version.patch, version.status, name) + +  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/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 0c97b38199..0000000000 --- a/platform/javascript/engine/utils.js +++ /dev/null @@ -1,51 +0,0 @@ -var Utils = { - -	createLocateRewrite: function(execName) { -		function rw(path) { -			if (path.endsWith('.worker.js')) { -				return execName + '.worker.js'; -			} else if (path.endsWith('.js')) { -				return execName + '.js'; -			} else if (path.endsWith('.wasm')) { -				return execName + '.wasm'; -			} -		} -		return rw; -	}, - -	createInstantiatePromise: function(wasmLoader) { -		function instantiateWasm(imports, onSuccess) { -			wasmLoader.then(function(xhr) { -				WebAssembly.instantiate(xhr.response, imports).then(function(result) { -					onSuccess(result['instance'], result['module']); -				}); -			}); -			wasmLoader = null; -			return {}; -		}; - -		return instantiateWasm; -	}, - -	findCanvas: function() { -		var nodes = document.getElementsByTagName('canvas'); -		if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { -			return nodes[0]; -		} -		throw new Error("No canvas found"); -	}, - -	isWebGLAvailable: function(majorVersion = 1) { - -		var testContext = false; -		try { -			var testCanvas = document.createElement('canvas'); -			if (majorVersion === 1) { -				testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); -			} else if (majorVersion === 2) { -				testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2'); -			} -		} catch (e) {} -		return !!testContext; -	} -}; diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp index a83ff44d20..f5d8a68994 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       */ @@ -37,16 +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"; @@ -127,6 +135,7 @@ public:  		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 += "Cache-Control: no-store, max-age=0\r\n";  		s += "\r\n";  		CharString cs = s.utf8();  		Error err = connection->put_data((const uint8_t *)cs.get_data(), cs.size() - 1); @@ -200,15 +209,40 @@ class EditorExportPlatformJavaScript : public EditorExportPlatform {  	Ref<ImageTexture> logo;  	Ref<ImageTexture> run_icon;  	Ref<ImageTexture> stop_icon; -	int menu_options; - -	void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags); +	int menu_options = 0; -private:  	Ref<EditorHTTPServer> server; -	bool server_quit; +	bool server_quit = false;  	Mutex server_lock; -	Thread *server_thread; +	Thread server_thread; + +	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); @@ -247,7 +281,7 @@ public:  	~EditorExportPlatformJavaScript();  }; -void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags) { +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"); @@ -255,6 +289,10 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re  	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]; @@ -262,6 +300,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re  		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"; @@ -288,16 +327,25 @@ 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::BOOL, "html/full_window_size"), true)); -	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"), ""));  }  String EditorExportPlatformJavaScript::get_name() const { @@ -315,11 +363,11 @@ Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const {  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")); @@ -347,7 +395,7 @@ bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p  		}  	} -	if (!err.empty()) { +	if (!err.is_empty()) {  		r_error = err;  	} @@ -372,11 +420,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())) { @@ -388,12 +433,24 @@ 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); @@ -429,17 +486,24 @@ 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, p_flags); +			_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") {  			file = p_path.get_file().get_basename() + ".worker.js"; +		} 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";  		} @@ -457,7 +521,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); @@ -467,7 +531,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, p_flags); +		_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) { @@ -480,7 +544,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) { @@ -501,7 +565,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) { @@ -566,8 +630,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; @@ -616,8 +682,7 @@ void EditorExportPlatformJavaScript::_server_thread_poll(void *data) {  EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() {  	server.instance(); -	server_quit = false; -	server_thread = Thread::create(_server_thread_poll, this); +	server_thread.start(_server_thread_poll, this);  	Ref<Image> img = memnew(Image(_javascript_logo));  	logo.instance(); @@ -633,15 +698,12 @@ EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() {  	} else {  		stop_icon.instance();  	} - -	menu_options = 0;  }  EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() {  	server->stop();  	server_quit = true; -	Thread::wait_to_finish(server_thread); -	memdelete(server_thread); +	server_thread.wait_to_finish();  }  void register_javascript_exporter() { 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 index f7f26e5262..54fc8fa3b5 100644 --- a/platform/javascript/godot_audio.h +++ b/platform/javascript/godot_audio.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       */ @@ -38,19 +38,24 @@ extern "C" {  #include "stddef.h"  extern int godot_audio_is_available(); - -extern int godot_audio_init(int p_mix_rate, int p_latency); -extern int godot_audio_create_processor(int p_buffer_length, int p_channel_count); - -extern void godot_audio_start(float *r_buffer_ptr); +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 void godot_audio_finish_async(); -extern float godot_audio_get_latency(); - -extern void godot_audio_capture_start(); +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 diff --git a/platform/javascript/godot_js.h b/platform/javascript/godot_js.h new file mode 100644 index 0000000000..0006848756 --- /dev/null +++ b/platform/javascript/godot_js.h @@ -0,0 +1,93 @@ +/*************************************************************************/ +/*  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 gamepad +extern char *godot_js_display_gamepad_cb(void (*p_on_change)(int p_index, int p_connected, const char *p_id, const char *p_guid)); +extern int godot_js_display_gamepad_sample(); +extern int godot_js_display_gamepad_sample_count(); +extern int godot_js_display_gamepad_sample_get(int p_idx, float r_btns[16], int32_t *r_btns_num, float r_axes[10], int32_t *r_axes_num, int32_t *r_standard); + +// 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 4d5ff88bdd..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       */ diff --git a/platform/javascript/http_client_javascript.cpp b/platform/javascript/http_client_javascript.cpp index cb0e48b8a9..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       */ @@ -78,15 +78,15 @@ Error HTTPClient::prepare_request(Method p_method, const String &p_url, const Ve  	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(": "); @@ -132,7 +132,7 @@ HTTPClient::Status HTTPClient::get_status() const {  }  bool HTTPClient::has_response() const { -	return !polled_response_header.empty(); +	return !polled_response_header.is_empty();  }  bool HTTPClient::is_response_chunked() const { @@ -145,7 +145,7 @@ int HTTPClient::get_response_code() const {  }  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); @@ -220,13 +220,13 @@ Error HTTPClient::poll() {  				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); 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 3a72b10dd4..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,95 +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) { -	p_arr->resize(p_len); -	*r_write = p_arr->write; -	return p_arr->ptrw(); +extern "C" { +union js_eval_ret { +	uint32_t b; +	double d; +	char *s; +}; + +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: @@ -130,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 01722c4bc8..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       */ @@ -36,20 +36,11 @@  #include <emscripten/emscripten.h>  #include <stdlib.h> +#include "godot_js.h" +  static OS_JavaScript *os = nullptr;  static uint64_t target_ticks = 0; -extern "C" EMSCRIPTEN_KEEPALIVE void _request_quit_callback(char *p_filev[], int p_filec) { -	DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); -	if (ds) { -		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); -	} -} -  void exit_callback() {  	emscripten_cancel_main_loop(); // After this, we can exit!  	Main::cleanup(); @@ -59,6 +50,10 @@ void exit_callback() {  	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(); @@ -74,68 +69,14 @@ void main_loop_callback() {  		target_ticks += (uint64_t)(1000000 / target_fps);  	}  	if (os->main_loop_iterate()) { -		emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async. -		/* clang-format off */ -		EM_ASM({ -			// This will contain the list of operations that need to complete before cleanup. -			Module.async_finish = [ -				// Always contains at least one async promise, to avoid firing immediately if nothing is added. -				new Promise(function(accept, reject) { -					setTimeout(accept, 0); -				}) -			]; -		}); -		/* clang-format on */ -		os->get_main_loop()->finish(); -		os->finalize_async(); // Will add all the async finish functions. -		/* clang-format off */ -		EM_ASM({ -			Promise.all(Module.async_finish).then(function() { -				Module.async_finish = []; -				return new Promise(function(accept, reject) { -					if (!Module.idbfs) { -						accept(); -						return; -					} -					FS.syncfs(function(error) { -						if (error) { -							err('Failed to save IDB file system: ' + error.message); -						} -						accept(); -					}); -				}); -			}).then(function() { -				ccall("cleanup_after_sync", null, []); -			}); -		}); -		/* clang-format on */ +		emscripten_cancel_main_loop(); // Cancel current loop and wait for cleanup_after_sync. +		godot_js_os_finish_async(cleanup_after_sync);  	}  } -extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() { -	emscripten_set_main_loop(exit_callback, -1, false); -} -  /// When calling main, it is assumed FS is setup and synced. -int main(int argc, char *argv[]) { -	// Configure locale. -	char locale_ptr[16]; -	/* clang-format off */ -	EM_ASM({ -		stringToUTF8(Module['locale'], $0, 16); -	}, locale_ptr); -	/* clang-format on */ -	setenv("LANG", locale_ptr, true); - -	// Ensure the canvas ID. -	/* clang-format off */ -	EM_ASM({ -		stringToUTF8("#" + Module['canvas'].id, $0, 255); -	}, DisplayServerJavaScript::canvas_id); -	/* clang-format on */ - +extern EMSCRIPTEN_KEEPALIVE int godot_js_main(int argc, char *argv[]) {  	os = new OS_JavaScript(); -	os->set_idb_available((bool)EM_ASM_INT({ return Module.idbfs }));  	// We must override main when testing is enabled  	TEST_MAIN_OVERRIDE @@ -146,15 +87,7 @@ int main(int argc, char *argv[]) {  	ResourceLoader::set_abort_on_missing_resources(false);  	Main::start(); -	os->get_main_loop()->init(); -	// Expose method for requesting quit. -	/* clang-format off */ -	EM_ASM({ -		Module['request_quit'] = function() { -			ccall("_request_quit_callback", null, []); -		}; -	}); -	/* clang-format on */ +	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. diff --git a/platform/javascript/native/id_handler.js b/platform/javascript/javascript_runtime.cpp index 67d29075b8..2996e95a95 100644 --- a/platform/javascript/native/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/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/engine/engine.js b/platform/javascript/js/engine/engine.js index adcd919a6b..01232cbece 100644 --- a/platform/javascript/engine/engine.js +++ b/platform/javascript/js/engine/engine.js @@ -1,14 +1,14 @@ -Function('return this')()['Engine'] = (function() { -	var preloader = new Preloader(); - -	var wasmExt = '.wasm'; -	var unloadAfterInit = true; -	var loadPath = ''; -	var loadPromise = null; -	var initPromise = null; -	var stderr = null; -	var stdout = null; -	var progressFunc = null; +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) { @@ -18,14 +18,14 @@ Function('return this')()['Engine'] = (function() {  			requestAnimationFrame(preloader.animateProgress);  		}  		return loadPromise; -	}; +	}  	function unload() {  		loadPromise = null; -	}; +	}  	/** @constructor */ -	function Engine() { +	function Engine() { // eslint-disable-line no-shadow  		this.canvas = null;  		this.executableName = '';  		this.rtenv = null; @@ -33,31 +33,38 @@ Function('return this')()['Engine'] = (function() {  		this.resizeCanvasOnStart = false;  		this.onExecute = null;  		this.onExit = null; -		this.persistentPaths = []; -	}; +		this.persistentPaths = ['/userfs']; +		this.gdnativeLibs = []; +	} -	Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { +	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.")); +				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') +		let config = {}; +		if (typeof stdout === 'function') {  			config.print = stdout; -		if (typeof stderr === 'function') +		} +		if (typeof stderr === 'function') {  			config.printErr = stderr; -		var me = this; -		initPromise = new Promise(function(resolve, reject) { +		} +		const me = this; +		initPromise = new Promise(function (resolve, reject) {  			config['locateFile'] = Utils.createLocateRewrite(loadPath);  			config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); -			Godot(config).then(function(module) { -				module['initFS'](me.persistentPaths).then(function(fs_err) { +			// Emscripten configuration. +			config['thisProgram'] = me.executableName; +			config['noExitRuntime'] = true; +			config['dynamicLibraries'] = [`${me.executableName}.side.wasm`].concat(me.gdnativeLibs); +			Godot(config).then(function (module) { +				module['initFS'](me.persistentPaths).then(function (fs_err) {  					me.rtenv = module;  					if (unloadAfterInit) {  						unload(); @@ -71,25 +78,28 @@ Function('return this')()['Engine'] = (function() {  	};  	/** @type {function(string, string):Object} */ -	Engine.prototype.preloadFile = function(file, path) { +	Engine.prototype.preloadFile = function (file, path) {  		return preloader.preload(file, path);  	};  	/** @type {function(...string):Object} */ -	Engine.prototype.start = function() { +	Engine.prototype.start = function () {  		// Start from arguments. -		var args = []; -		for (var i = 0; i < arguments.length; i++) { +		const args = []; +		for (let i = 0; i < arguments.length; i++) {  			args.push(arguments[i]);  		} -		var me = this; -		return me.init().then(function() { +		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. @@ -98,35 +108,45 @@ Function('return this')()['Engine'] = (function() {  			}  			// Disable right-click context menu. -			me.canvas.addEventListener('contextmenu', function(ev) { +			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"); +			me.canvas.addEventListener('webglcontextlost', function (ev) { +				alert('WebGL context lost, please reload the page'); // eslint-disable-line no-alert  				ev.preventDefault();  			}, false);  			// Browser locale, or custom one if defined. -			var locale = me.customLocale; +			let locale = me.customLocale;  			if (!locale) {  				locale = navigator.languages ? navigator.languages[0] : navigator.language;  				locale = locale.split('.')[0];  			} -			me.rtenv['locale'] = locale; -			me.rtenv['canvas'] = me.canvas; -			me.rtenv['thisProgram'] = me.executableName; -			me.rtenv['resizeCanvasOnStart'] = me.resizeCanvasOnStart; -			me.rtenv['noExitRuntime'] = true; -			me.rtenv['onExecute'] = me.onExecute; -			me.rtenv['onExit'] = function(code) { -				if (me.onExit) -					me.onExit(code); -				me.rtenv = null; -			}; -			return new Promise(function(resolve, reject) { -				preloader.preloadedFiles.forEach(function(file) { +			// 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 @@ -137,96 +157,110 @@ Function('return this')()['Engine'] = (function() {  		});  	}; -	Engine.prototype.startGame = function(execName, mainPack, extraArgs) { +	Engine.prototype.startGame = function (execName, mainPack, extraArgs) {  		// Start and init with execName as loadPath if not inited.  		this.executableName = execName; -		var me = this; +		const me = this;  		return Promise.all([  			this.init(execName), -			this.preloadFile(mainPack, mainPack) -		]).then(function() { -			var args = ['--main-pack', mainPack]; -			if (extraArgs) +			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) { +	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) { +	Engine.prototype.setUnloadAfterInit = function (enabled) {  		unloadAfterInit = enabled;  	}; -	Engine.prototype.setCanvas = function(canvasElem) { +	Engine.prototype.setCanvas = function (canvasElem) {  		this.canvas = canvasElem;  	}; -	Engine.prototype.setCanvasResizedOnStart = function(enabled) { +	Engine.prototype.setCanvasResizedOnStart = function (enabled) {  		this.resizeCanvasOnStart = enabled;  	}; -	Engine.prototype.setLocale = function(locale) { +	Engine.prototype.setLocale = function (locale) {  		this.customLocale = locale;  	}; -	Engine.prototype.setExecutableName = function(newName) { +	Engine.prototype.setExecutableName = function (newName) {  		this.executableName = newName;  	}; -	Engine.prototype.setProgressFunc = function(func) { +	Engine.prototype.setProgressFunc = function (func) {  		progressFunc = func;  	}; -	Engine.prototype.setStdoutFunc = function(func) { -		var print = function(text) { +	Engine.prototype.setStdoutFunc = function (func) { +		const print = function (text) { +			let msg = text;  			if (arguments.length > 1) { -				text = Array.prototype.slice.call(arguments).join(" "); +				msg = Array.prototype.slice.call(arguments).join(' ');  			} -			func(text); +			func(msg);  		}; -		if (this.rtenv) +		if (this.rtenv) {  			this.rtenv.print = print; +		}  		stdout = print;  	}; -	Engine.prototype.setStderrFunc = function(func) { -		var printErr = function(text) { -			if (arguments.length > 1) -				text = Array.prototype.slice.call(arguments).join(" "); -			func(text); +	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) +		if (this.rtenv) {  			this.rtenv.printErr = printErr; +		}  		stderr = printErr;  	}; -	Engine.prototype.setOnExecute = function(onExecute) { -		if (this.rtenv) -			this.rtenv.onExecute = onExecute; +	Engine.prototype.setOnExecute = function (onExecute) {  		this.onExecute = onExecute;  	}; -	Engine.prototype.setOnExit = function(onExit) { +	Engine.prototype.setOnExit = function (onExit) {  		this.onExit = onExit;  	}; -	Engine.prototype.copyToFS = function(path, buffer) { +	Engine.prototype.copyToFS = function (path, buffer) {  		if (this.rtenv == null) { -			throw new Error("Engine must be inited before copying files"); +			throw new Error('Engine must be inited before copying files');  		}  		this.rtenv['copyToFS'](path, buffer);  	}; -	Engine.prototype.setPersistentPaths = function(persistentPaths) { +	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; @@ -249,5 +283,10 @@ Function('return this')()['Engine'] = (function() {  	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..2977b7c122 --- /dev/null +++ b/platform/javascript/js/libs/library_godot_display.js @@ -0,0 +1,667 @@ +/*************************************************************************/ +/*  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 Gamepad API helper. + */ +const GodotDisplayGamepads = { +	$GodotDisplayGamepads__deps: ['$GodotRuntime', '$GodotDisplayListeners'], +	$GodotDisplayGamepads: { +		samples: [], + +		get_pads: function () { +			try { +				// Will throw in iframe when permission is denied. +				// Will throw/warn in the future for insecure contexts. +				// See https://github.com/w3c/gamepad/pull/120 +				const pads = navigator.getGamepads(); +				if (pads) { +					return pads; +				} +				return []; +			} catch (e) { +				return []; +			} +		}, + +		get_samples: function () { +			return GodotDisplayGamepads.samples; +		}, + +		get_sample: function (index) { +			const samples = GodotDisplayGamepads.samples; +			return index < samples.length ? samples[index] : null; +		}, + +		sample: function () { +			const pads = GodotDisplayGamepads.get_pads(); +			const samples = []; +			for (let i = 0; i < pads.length; i++) { +				const pad = pads[i]; +				if (!pad) { +					samples.push(null); +					continue; +				} +				const s = { +					standard: pad.mapping === 'standard', +					buttons: [], +					axes: [], +					connected: pad.connected, +				}; +				for (let b = 0; b < pad.buttons.length; b++) { +					s.buttons.push(pad.buttons[b].value); +				} +				for (let a = 0; a < pad.axes.length; a++) { +					s.axes.push(pad.axes[a]); +				} +				samples.push(s); +			} +			GodotDisplayGamepads.samples = samples; +		}, + +		init: function (onchange) { +			GodotDisplayListeners.samples = []; +			function add(pad) { +				const guid = GodotDisplayGamepads.get_guid(pad); +				const c_id = GodotRuntime.allocString(pad.id); +				const c_guid = GodotRuntime.allocString(guid); +				onchange(pad.index, 1, c_id, c_guid); +				GodotRuntime.free(c_id); +				GodotRuntime.free(c_guid); +			} +			const pads = GodotDisplayGamepads.get_pads(); +			for (let i = 0; i < pads.length; i++) { +				// Might be reserved space. +				if (pads[i]) { +					add(pads[i]); +				} +			} +			GodotDisplayListeners.add(window, 'gamepadconnected', function (evt) { +				add(evt.gamepad); +			}, false); +			GodotDisplayListeners.add(window, 'gamepaddisconnected', function (evt) { +				onchange(evt.gamepad.index, 0); +			}, false); +		}, + +		get_guid: function (pad) { +			if (pad.mapping) { +				return pad.mapping; +			} +			const ua = navigator.userAgent; +			let os = 'Unknown'; +			if (ua.indexOf('Android') >= 0) { +				os = 'Android'; +			} else if (ua.indexOf('Linux') >= 0) { +				os = 'Linux'; +			} else if (ua.indexOf('iPhone') >= 0) { +				os = 'iOS'; +			} else if (ua.indexOf('Macintosh') >= 0) { +				// Updated iPads will fall into this category. +				os = 'MacOSX'; +			} else if (ua.indexOf('Windows') >= 0) { +				os = 'Windows'; +			} + +			const id = pad.id; +			// Chrom* style: NAME (Vendor: xxxx Product: xxxx) +			const exp1 = /vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i; +			// Firefox/Safari style (safari may remove leading zeores) +			const exp2 = /^([0-9a-f]+)-([0-9a-f]+)-/i; +			let vendor = ''; +			let product = ''; +			if (exp1.test(id)) { +				const match = exp1.exec(id); +				vendor = match[1].padStart(4, '0'); +				product = match[2].padStart(4, '0'); +			} else if (exp2.test(id)) { +				const match = exp2.exec(id); +				vendor = match[1].padStart(4, '0'); +				product = match[2].padStart(4, '0'); +			} +			if (!vendor || !product) { +				return `${os}Unknown`; +			} +			return os + vendor + product; +		}, +	}, +}; +mergeInto(LibraryManager.library, GodotDisplayGamepads); + +/** + * Display server interface. + * + * Exposes all the functions needed by DisplayServer implementation. + */ +const GodotDisplay = { +	$GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop', '$GodotDisplayGamepads'], +	$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)); +	}, + +	/* +	 * Gamepads +	 */ +	godot_js_display_gamepad_cb__sig: 'vi', +	godot_js_display_gamepad_cb: function (change_cb) { +		const onchange = GodotRuntime.get_func(change_cb); +		GodotDisplayGamepads.init(onchange); +	}, + +	godot_js_display_gamepad_sample_count__sig: 'i', +	godot_js_display_gamepad_sample_count: function () { +		return GodotDisplayGamepads.get_samples().length; +	}, + +	godot_js_display_gamepad_sample__sig: 'i', +	godot_js_display_gamepad_sample: function () { +		GodotDisplayGamepads.sample(); +		return 0; +	}, + +	godot_js_display_gamepad_sample_get__sig: 'iiiiiii', +	godot_js_display_gamepad_sample_get: function (p_index, r_btns, r_btns_num, r_axes, r_axes_num, r_standard) { +		const sample = GodotDisplayGamepads.get_sample(p_index); +		if (!sample || !sample.connected) { +			return 1; +		} +		const btns = sample.buttons; +		const btns_len = btns.length < 16 ? btns.length : 16; +		for (let i = 0; i < btns_len; i++) { +			GodotRuntime.setHeapValue(r_btns + (i << 2), btns[i], 'float'); +		} +		GodotRuntime.setHeapValue(r_btns_num, btns_len, 'i32'); +		const axes = sample.axes; +		const axes_len = axes.length < 10 ? axes.length : 10; +		for (let i = 0; i < axes_len; i++) { +			GodotRuntime.setHeapValue(r_axes + (i << 2), axes[i], 'float'); +		} +		GodotRuntime.setHeapValue(r_axes_num, axes_len, 'i32'); +		const is_standard = sample.standard ? 1 : 0; +		GodotRuntime.setHeapValue(r_standard, is_standard, 'i32'); +		return 0; +	}, +}; + +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/native/http_request.js b/platform/javascript/js/libs/library_godot_http_request.js index f621689f9d..930d3209f8 100644 --- a/platform/javascript/native/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/native/library_godot_audio.js b/platform/javascript/native/library_godot_audio.js deleted file mode 100644 index d300280ccd..0000000000 --- a/platform/javascript/native/library_godot_audio.js +++ /dev/null @@ -1,173 +0,0 @@ -/*************************************************************************/ -/*  library_godot_audio.js                                               */ -/*************************************************************************/ -/*                       This file is part of:                           */ -/*                           GODOT ENGINE                                */ -/*                      https://godotengine.org                          */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */ -/*                                                                       */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the       */ -/* "Software"), to deal in the Software without restriction, including   */ -/* without limitation the rights to use, copy, modify, merge, publish,   */ -/* distribute, sublicense, and/or sell copies of the Software, and to    */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions:                                             */ -/*                                                                       */ -/* The above copyright notice and this permission notice shall be        */ -/* included in all copies or substantial portions of the Software.       */ -/*                                                                       */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */ -/*************************************************************************/ -var GodotAudio = { - -	$GodotAudio: { - -		ctx: null, -		input: null, -		script: null, -	}, - -	godot_audio_is_available__proxy: 'sync', -	godot_audio_is_available: function () { -		if (!(window.AudioContext || window.webkitAudioContext)) { -			return 0; -		} -		return 1; -	}, - -	godot_audio_init: function(mix_rate, latency) { -		GodotAudio.ctx = new (window.AudioContext || window.webkitAudioContext)({ -			sampleRate: mix_rate, -			latencyHint: latency -		}); -		return GodotAudio.ctx.destination.channelCount; -	}, - -	godot_audio_create_processor: function(buffer_length, channel_count) { -		GodotAudio.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); -		GodotAudio.script.connect(GodotAudio.ctx.destination); -		return GodotAudio.script.bufferSize; -	}, - -	godot_audio_start: function(buffer_ptr) { -		var audioDriverProcessStart = cwrap('audio_driver_process_start'); -		var audioDriverProcessEnd = cwrap('audio_driver_process_end'); -		var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']); -		GodotAudio.script.onaudioprocess = function(audioProcessingEvent) { -			audioDriverProcessStart(); - -			var input = audioProcessingEvent.inputBuffer; -			var output = audioProcessingEvent.outputBuffer; -			var internalBuffer = HEAPF32.subarray( -					buffer_ptr / HEAPF32.BYTES_PER_ELEMENT, -					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 (GodotAudio.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]); -				} -			} -			audioDriverProcessEnd(); -		}; -	}, - -	godot_audio_resume: function() { -		if (GodotAudio.ctx && GodotAudio.ctx.state != 'running') { -			GodotAudio.ctx.resume(); -		} -	}, - -	godot_audio_finish_async: function() { -		Module.async_finish.push(new Promise(function(accept, reject) { -			if (!GodotAudio.ctx) { -				setTimeout(accept, 0); -			} else { -				if (GodotAudio.script) { -					GodotAudio.script.disconnect(); -					GodotAudio.script = null; -				} -				if (GodotAudio.input) { -					GodotAudio.input.disconnect(); -					GodotAudio.input = null; -				} -				GodotAudio.ctx.close().then(function() { -					accept(); -				}).catch(function(e) { -					accept(); -				}); -				GodotAudio.ctx = null; -			} -		})); -	}, - -	godot_audio_get_latency__proxy: 'sync', -	godot_audio_get_latency: function() { -		var latency = 0; -		if (GodotAudio.ctx) { -			if (GodotAudio.ctx.baseLatency) { -				latency += GodotAudio.ctx.baseLatency; -			} -			if (GodotAudio.ctx.outputLatency) { -				latency += GodotAudio.ctx.outputLatency; -			} -		} -		return latency; -	}, - -	godot_audio_capture_start__proxy: 'sync', -	godot_audio_capture_start: function() { -		if (GodotAudio.input) { -			return; // Already started. -		} -		function gotMediaInput(stream) { -			GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); -			GodotAudio.input.connect(GodotAudio.script); -		} - -		function gotMediaInputError(e) { -			out(e); -		} - -		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); -		} -	}, - -	godot_audio_capture_stop__proxy: 'sync', -	godot_audio_capture_stop: function() { -		if (GodotAudio.input) { -			const tracks = GodotAudio.input.mediaStream.getTracks(); -			for (var i = 0; i < tracks.length; i++) { -				tracks[i].stop(); -			} -			GodotAudio.input.disconnect(); -			GodotAudio.input = null; -		} -	}, -}; - -autoAddDeps(GodotAudio, "$GodotAudio"); -mergeInto(LibraryManager.library, GodotAudio); diff --git a/platform/javascript/native/utils.js b/platform/javascript/native/utils.js deleted file mode 100644 index 0b3698fd86..0000000000 --- a/platform/javascript/native/utils.js +++ /dev/null @@ -1,277 +0,0 @@ -/*************************************************************************/ -/*  utils.js                                                             */ -/*************************************************************************/ -/*                       This file is part of:                           */ -/*                           GODOT ENGINE                                */ -/*                      https://godotengine.org                          */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */ -/*                                                                       */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the       */ -/* "Software"), to deal in the Software without restriction, including   */ -/* without limitation the rights to use, copy, modify, merge, publish,   */ -/* distribute, sublicense, and/or sell copies of the Software, and to    */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions:                                             */ -/*                                                                       */ -/* The above copyright notice and this permission notice shall be        */ -/* included in all copies or substantial portions of the Software.       */ -/*                                                                       */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */ -/*************************************************************************/ - -Module['initFS'] = function(persistentPaths) { -	FS.mkdir('/userfs'); -	FS.mount(IDBFS, {}, '/userfs'); - -	function createRecursive(dir) { -		try { -			FS.stat(dir); -		} catch (e) { -			if (e.errno !== ERRNO_CODES.ENOENT) { -				throw e; -			} -			FS.mkdirTree(dir); -		} -	} - -	persistentPaths.forEach(function(path) { -		createRecursive(path); -		FS.mount(IDBFS, {}, path); -	}); -	return new Promise(function(resolve, reject) { -		FS.syncfs(true, function(err) { -			if (err) { -				Module.idbfs = false; -				console.log("IndexedDB not available: " + err.message); -			} else { -				Module.idbfs = true; -			} -			resolve(err); -		}); -	}); -}; - -Module['copyToFS'] = function(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 !== ERRNO_CODES.ENOENT) { -			throw e; -		} -		FS.mkdirTree(dir); -	} -	// With memory growth, canOwn should be false. -	FS.writeFile(path, new Uint8Array(buffer), {'flags': 'wx+'}); -} - -Module.drop_handler = (function() { -	var upload = []; -	var uploadPromises = []; -	var uploadCallback = null; - -	function readFilePromise(entry, path) { -		return new Promise(function(resolve, reject) { -			entry.file(function(file) { -				var reader = new FileReader(); -				reader.onload = function() { -					var 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']; -					upload.push(f); -					resolve() -				}; -				reader.onerror = function() { -					console.log("Error reading file"); -					reject(); -				} - -				reader.readAsArrayBuffer(file); - -				}, function(err) { -					console.log("Error!"); -					reject(); -				}); -		}); -	} - -	function readDirectoryPromise(entry) { -		return new Promise(function(resolve, reject) { -			var reader = entry.createReader(); -			reader.readEntries(function(entries) { -				for (var i = 0; i < entries.length; i++) { -					var ent = entries[i]; -					if (ent.isDirectory) { -						uploadPromises.push(readDirectoryPromise(ent)); -					} else if (ent.isFile) { -						uploadPromises.push(readFilePromise(ent)); -					} -				} -				resolve(); -			}); -		}); -	} - -	function processUploadsPromises(resolve, reject) { -		if (uploadPromises.length == 0) { -			resolve(); -			return; -		} -		uploadPromises.pop().then(function() { -			setTimeout(function() { -				processUploadsPromises(resolve, reject); -				//processUploadsPromises.bind(null, resolve, reject) -			}, 0); -		}); -	} - -	function dropFiles(files) { -		var args = files || []; -		var argc = args.length; -		var argv = stackAlloc((argc + 1) * 4); -		for (var i = 0; i < argc; i++) { -			HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i]); -		} -		HEAP32[(argv >> 2) + argc] = 0; -		// Defined in display_server_javascript.cpp -		ccall('_drop_files_callback', 'void', ['number', 'number'], [argv, argc]); -	} - -	return function(ev) { -		ev.preventDefault(); -		if (ev.dataTransfer.items) { -			// Use DataTransferItemList interface to access the file(s) -			for (var i = 0; i < ev.dataTransfer.items.length; i++) { -				const item = ev.dataTransfer.items[i]; -				var entry = null; -				if ("getAsEntry" in item) { -					entry = item.getAsEntry(); -				} else if ("webkitGetAsEntry" in item) { -					entry = item.webkitGetAsEntry(); -				} -				if (!entry) { -					console.error("File upload not supported"); -				} else if (entry.isDirectory) { -					uploadPromises.push(readDirectoryPromise(entry)); -				} else if (entry.isFile) { -					uploadPromises.push(readFilePromise(entry)); -				} else { -					console.error("Unrecognized entry...", entry); -				} -			} -		} else { -			console.error("File upload not supported"); -		} -		uploadCallback = new Promise(processUploadsPromises).then(function() { -			const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/"; -			var drops = []; -			var files = []; -			upload.forEach((elem) => { -				var path = elem['path']; -				Module['copyToFS'](DROP + path, elem['data']); -				var idx = path.indexOf("/"); -				if (idx == -1) { -					// Root file -					drops.push(DROP + path); -				} else { -					// Subdir -					var sub = path.substr(0, idx); -					idx = sub.indexOf("/"); -					if (idx < 0 && drops.indexOf(DROP + sub) == -1) { -						drops.push(DROP + sub); -					} -				} -				files.push(DROP + path); -			}); -			uploadPromises = []; -			upload = []; -			dropFiles(drops); -			var dirs = [DROP.substr(0, DROP.length -1)]; -			files.forEach(function (file) { -				FS.unlink(file); -				var dir = file.replace(DROP, ""); -				var 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 = dirs.sort(function(a, b) { -				var al = (a.match(/\//g) || []).length; -				var bl = (b.match(/\//g) || []).length; -				if (al > bl) -					return -1; -				else if (al < bl) -					return 1; -				return 0; -			}); -			dirs.forEach(function(dir) { -				FS.rmdir(dir); -			}); -		}); -	} -})(); - -function EventHandlers() { -	function Handler(target, event, method, capture) { -		this.target = target; -		this.event = event; -		this.method = method; -		this.capture = capture; -	} - -	var listeners = []; - -	function has(target, event, method, capture) { -		return listeners.findIndex(function(e) { -			return e.target === target && e.event === event && e.method === method && e.capture == capture; -		}) !== -1; -	} - -	this.add = function(target, event, method, capture) { -		if (has(target, event, method, capture)) { -			return; -		} -		listeners.push(new Handler(target, event, method, capture)); -		target.addEventListener(event, method, capture); -	}; - -	this.remove = function(target, event, method, capture) { -		if (!has(target, event, method, capture)) { -			return; -		} -		target.removeEventListener(event, method, capture); -	}; - -	this.clear = function() { -		listeners.forEach(function(h) { -			h.target.removeEventListener(h.event, h.method, h.capture); -		}); -		listeners.length = 0; -	}; -} - -Module.listeners = new EventHandlers(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index cf5751f384..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       */ @@ -31,7 +31,6 @@  #include "os_javascript.h"  #include "core/debugger/engine_debugger.h" -#include "core/io/file_access_buffered_fa.h"  #include "core/io/json.h"  #include "drivers/unix/dir_access_unix.h"  #include "drivers/unix/file_access_unix.h" @@ -43,13 +42,15 @@  #include "modules/websocket/remote_debugger_peer_websocket.h"  #endif +#include <dlfcn.h>  #include <emscripten.h>  #include <stdlib.h> +#include "godot_js.h" +  // Lifecycle  void OS_JavaScript::initialize() {  	OS_Unix::initialize_core(); -	FileAccess::make_default<FileAccessBufferedFA<FileAccessUnix>>(FileAccess::ACCESS_RESOURCES);  	DisplayServerJavaScript::register_javascript_driver();  #ifdef MODULE_WEBSOCKET_ENABLED @@ -72,24 +73,15 @@ MainLoop *OS_JavaScript::get_main_loop() const {  	return main_loop;  } -extern "C" EMSCRIPTEN_KEEPALIVE void _idb_synced() { -	OS_JavaScript::get_singleton()->idb_is_syncing = false; +void OS_JavaScript::fs_sync_callback() { +	get_singleton()->idb_is_syncing = false;  }  bool OS_JavaScript::main_loop_iterate() {  	if (is_userfs_persistent() && idb_needs_sync && !idb_is_syncing) {  		idb_is_syncing = true;  		idb_needs_sync = false; -		/* clang-format off */ -		EM_ASM({ -			FS.syncfs(function(error) { -				if (error) { -					err('Failed to save IDB file system: ' + error.message); -				} -				ccall("_idb_synced", 'void', [], []); -			}); -		}); -		/* clang-format on */ +		godot_js_os_fs_sync(&fs_sync_callback);  	}  	DisplayServer::get_singleton()->process_events(); @@ -104,13 +96,6 @@ void OS_JavaScript::delete_main_loop() {  	main_loop = nullptr;  } -void OS_JavaScript::finalize_async() { -	finalizing = true; -	if (audio_driver_javascript) { -		audio_driver_javascript->finish_async(); -	} -} -  void OS_JavaScript::finalize() {  	delete_main_loop();  	if (audio_driver_javascript) { @@ -121,24 +106,18 @@ void OS_JavaScript::finalize() {  // 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); +} + +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); -	/* clang-format off */ -	int failed = EM_ASM_INT({ -		const json_args = UTF8ToString($0); -		const args = JSON.parse(json_args); -		if (Module["onExecute"]) { -			Module["onExecute"](args); -			return 0; -		} -		return 1; -	}, json_args.utf8().get_data()); -	/* clang-format on */ -	ERR_FAIL_COND_V_MSG(failed, ERR_UNAVAILABLE, "OS::execute() must be implemented in JavaScript via 'engine.setOnExecute' if required."); +	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;  } @@ -151,12 +130,24 @@ int OS_JavaScript::get_process_id() const {  }  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 +#ifndef NO_THREADS +	if (p_feature == "threads") {  		return true; +	} +#endif +#if WASM_GDNATIVE +	if (p_feature == "wasm32") { +		return true; +	}  #endif  	return false; @@ -168,11 +159,7 @@ String OS_JavaScript::get_executable_path() const {  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;  } @@ -211,14 +198,17 @@ void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags  	}  } -void OS_JavaScript::set_idb_available(bool p_idb_available) { -	idb_available = p_idb_available; -} -  bool OS_JavaScript::is_userfs_persistent() const {  	return idb_available;  } +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());  } @@ -227,11 +217,17 @@ void OS_JavaScript::initialize_joypads() {  }  OS_JavaScript::OS_JavaScript() { +	char locale_ptr[16]; +	godot_js_config_locale_get(locale_ptr, 16); +	setenv("LANG", locale_ptr, true); +  	if (AudioDriverJavaScript::is_available()) {  		audio_driver_javascript = memnew(AudioDriverJavaScript);  		AudioDriverManager::add_driver(audio_driver_javascript);  	} +	idb_available = godot_js_os_fs_is_persistent(); +  	Vector<Logger *> loggers;  	loggers.push_back(memnew(StdLogger));  	_set_logger(memnew(CompositeLogger(loggers))); diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 85551d708b..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       */ @@ -42,13 +42,14 @@ class OS_JavaScript : public OS_Unix {  	MainLoop *main_loop = nullptr;  	AudioDriverJavaScript *audio_driver_javascript = nullptr; -	bool finalizing = false; +	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:  	void initialize() override; @@ -61,18 +62,16 @@ protected:  	bool _check_internal_feature_support(const String &p_feature) override;  public: -	bool idb_is_syncing = false; -  	// Override return type to make writing static callbacks less tedious.  	static OS_JavaScript *get_singleton();  	void initialize_joypads() override;  	MainLoop *get_main_loop() const override; -	void finalize_async();  	bool main_loop_iterate(); -	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) override; +	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; @@ -88,11 +87,10 @@ public:  	String get_data_path() const override;  	String get_user_data_dir() const override; -	void set_idb_available(bool p_idb_available);  	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 resume_audio(); -	bool is_finalizing() { return finalizing; }  	OS_JavaScript();  }; 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       */ |