summaryrefslogtreecommitdiff
path: root/platform/javascript
diff options
context:
space:
mode:
Diffstat (limited to 'platform/javascript')
-rw-r--r--platform/javascript/api/api.cpp4
-rw-r--r--platform/javascript/detect.py5
-rw-r--r--platform/javascript/display_server_javascript.cpp42
-rw-r--r--platform/javascript/display_server_javascript.h111
-rw-r--r--platform/javascript/dom_keys.inc8
-rw-r--r--platform/javascript/emscripten_helpers.py5
-rw-r--r--platform/javascript/export/export.cpp964
-rw-r--r--platform/javascript/export/export.h5
-rw-r--r--platform/javascript/export/export_plugin.cpp671
-rw-r--r--platform/javascript/export/export_plugin.h149
-rw-r--r--platform/javascript/export/export_server.h254
-rw-r--r--platform/javascript/http_client.h.inc51
-rw-r--r--platform/javascript/http_client_javascript.cpp101
-rw-r--r--platform/javascript/http_client_javascript.h108
-rw-r--r--platform/javascript/javascript_main.cpp2
-rw-r--r--platform/javascript/javascript_singleton.cpp13
-rw-r--r--platform/javascript/js/engine/config.js10
-rw-r--r--platform/javascript/js/libs/audio.worklet.js2
-rw-r--r--platform/javascript/js/libs/library_godot_javascript_singleton.js15
-rw-r--r--platform/javascript/js/libs/library_godot_os.js5
-rw-r--r--platform/javascript/os_javascript.cpp13
-rw-r--r--platform/javascript/os_javascript.h3
22 files changed, 1362 insertions, 1179 deletions
diff --git a/platform/javascript/api/api.cpp b/platform/javascript/api/api.cpp
index 5ad2bf56cf..e7c018ba9f 100644
--- a/platform/javascript/api/api.cpp
+++ b/platform/javascript/api/api.cpp
@@ -37,8 +37,8 @@ static JavaScript *javascript_eval;
void register_javascript_api() {
JavaScriptToolsEditorPlugin::initialize();
- ClassDB::register_virtual_class<JavaScriptObject>();
- ClassDB::register_virtual_class<JavaScript>();
+ GDREGISTER_VIRTUAL_CLASS(JavaScriptObject);
+ GDREGISTER_VIRTUAL_CLASS(JavaScript);
javascript_eval = memnew(JavaScript);
Engine::get_singleton()->add_singleton(Engine::Singleton("JavaScript", javascript_eval));
}
diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py
index da6adc4cd8..173b558b6d 100644
--- a/platform/javascript/detect.py
+++ b/platform/javascript/detect.py
@@ -29,7 +29,7 @@ def get_opts():
from SCons.Variables import BoolVariable
return [
- ("initial_memory", "Initial WASM memory (in MiB)", 16),
+ ("initial_memory", "Initial WASM memory (in MiB)", 32),
BoolVariable("use_assertions", "Use Emscripten runtime assertions", False),
BoolVariable("use_thinlto", "Use ThinLTO", False),
BoolVariable("use_ubsan", "Use Emscripten undefined behavior sanitizer (UBSAN)", False),
@@ -38,7 +38,7 @@ def get_opts():
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("threads_enabled", "Enable WebAssembly Threads support (limited browser support)", True),
BoolVariable("gdnative_enabled", "Enable WebAssembly GDNative support (produces bigger binaries)", False),
BoolVariable("use_closure_compiler", "Use closure compiler to minimize JavaScript code", False),
]
@@ -130,7 +130,6 @@ def configure(env):
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
diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp
index 1cc05a2e19..fda18a5c19 100644
--- a/platform/javascript/display_server_javascript.cpp
+++ b/platform/javascript/display_server_javascript.cpp
@@ -128,7 +128,7 @@ void DisplayServerJavaScript::dom2godot_mod(T *emscripten_event_ptr, Ref<InputEv
Ref<InputEventKey> DisplayServerJavaScript::setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) {
Ref<InputEventKey> ev;
- ev.instance();
+ ev.instantiate();
ev->set_echo(emscripten_event->repeat);
dom2godot_mod(emscripten_event, ev);
ev->set_keycode(dom_code2godot_scancode(emscripten_event->code, emscripten_event->key, false));
@@ -172,7 +172,7 @@ EM_BOOL DisplayServerJavaScript::keyup_callback(int p_event_type, const Emscript
Ref<InputEventKey> ev = setup_key_event(p_event);
ev->set_pressed(false);
Input::get_singleton()->parse_input_event(ev);
- return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != 0;
+ return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != (Key)0;
}
// Mouse
@@ -181,7 +181,7 @@ EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const E
DisplayServerJavaScript *display = get_singleton();
Ref<InputEventMouseButton> ev;
- ev.instance();
+ ev.instantiate();
ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_MOUSEDOWN);
ev->set_position(compute_position_in_canvas(p_event->clientX, p_event->clientY));
ev->set_global_position(ev->get_position());
@@ -230,7 +230,7 @@ EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const E
Input *input = Input::get_singleton();
int mask = input->get_mouse_button_mask();
- int button_flag = 1 << (ev->get_button_index() - 1);
+ MouseButton button_flag = MouseButton(1 << (ev->get_button_index() - 1));
if (ev->is_pressed()) {
// Since the event is consumed, focus manually. The containing iframe,
// if exists, may not have focus yet, so focus even if already focused.
@@ -261,7 +261,7 @@ EM_BOOL DisplayServerJavaScript::mousemove_callback(int p_event_type, const Emsc
return false;
Ref<InputEventMouseMotion> ev;
- ev.instance();
+ ev.instantiate();
dom2godot_mod(p_event, ev);
ev->set_button_mask(input_mask);
@@ -452,7 +452,7 @@ EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const Emscript
Input *input = Input::get_singleton();
Ref<InputEventMouseButton> ev;
- ev.instance();
+ ev.instantiate();
ev->set_position(input->get_mouse_position());
ev->set_global_position(ev->get_position());
@@ -478,11 +478,11 @@ EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const Emscript
int button_flag = 1 << (ev->get_button_index() - 1);
ev->set_pressed(true);
- ev->set_button_mask(input->get_mouse_button_mask() | button_flag);
+ ev->set_button_mask(MouseButton(input->get_mouse_button_mask() | button_flag));
input->parse_input_event(ev);
ev->set_pressed(false);
- ev->set_button_mask(input->get_mouse_button_mask() & ~button_flag);
+ ev->set_button_mask(MouseButton(input->get_mouse_button_mask() & ~button_flag));
input->parse_input_event(ev);
return true;
@@ -492,7 +492,7 @@ EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const Emscript
EM_BOOL DisplayServerJavaScript::touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) {
DisplayServerJavaScript *display = get_singleton();
Ref<InputEventScreenTouch> ev;
- ev.instance();
+ ev.instantiate();
int lowest_id_index = -1;
for (int i = 0; i < p_event->numTouches; ++i) {
const EmscriptenTouchPoint &touch = p_event->touches[i];
@@ -514,7 +514,7 @@ EM_BOOL DisplayServerJavaScript::touch_press_callback(int p_event_type, const Em
EM_BOOL DisplayServerJavaScript::touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) {
DisplayServerJavaScript *display = get_singleton();
Ref<InputEventScreenDrag> ev;
- ev.instance();
+ ev.instantiate();
int lowest_id_index = -1;
for (int i = 0; i < p_event->numTouches; ++i) {
const EmscriptenTouchPoint &touch = p_event->touches[i];
@@ -553,12 +553,12 @@ void DisplayServerJavaScript::vk_input_text_callback(const char *p_text, int p_c
Input *input = Input::get_singleton();
Ref<InputEventKey> k;
for (int i = 0; i < p_cursor; i++) {
- k.instance();
+ k.instantiate();
k->set_pressed(true);
k->set_echo(false);
k->set_keycode(KEY_RIGHT);
input->parse_input_event(k);
- k.instance();
+ k.instantiate();
k->set_pressed(false);
k->set_echo(false);
k->set_keycode(KEY_RIGHT);
@@ -605,17 +605,17 @@ void DisplayServerJavaScript::process_joypads() {
Input::JoyAxisValue joy_axis;
joy_axis.min = 0;
joy_axis.value = value;
- int a = b == 6 ? JOY_AXIS_TRIGGER_LEFT : JOY_AXIS_TRIGGER_RIGHT;
+ JoyAxis 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);
+ input->joy_button(idx, (JoyButton)b, value);
}
}
for (int a = 0; a < s_axes_num; a++) {
Input::JoyAxisValue joy_axis;
joy_axis.min = -1;
joy_axis.value = s_axes[a];
- input->joy_axis(idx, a, joy_axis);
+ input->joy_axis(idx, (JoyAxis)a, joy_axis);
}
}
}
@@ -659,10 +659,6 @@ void DisplayServerJavaScript::send_window_event_callback(int p_notification) {
}
}
-void DisplayServerJavaScript::alert(const String &p_alert, const String &p_title) {
- godot_js_display_alert(p_alert.utf8().get_data());
-}
-
void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) {
ERR_FAIL_COND(p_icon.is_null());
Ref<Image> icon = p_icon;
@@ -710,18 +706,18 @@ void DisplayServerJavaScript::_dispatch_input_event(const Ref<InputEvent> &p_eve
}
}
-DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) {
- return memnew(DisplayServerJavaScript(p_rendering_driver, p_mode, p_flags, p_resolution, r_error));
+DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Size2i &p_resolution, Error &r_error) {
+ return memnew(DisplayServerJavaScript(p_rendering_driver, p_window_mode, p_vsync_mode, p_flags, p_resolution, r_error));
}
-DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) {
+DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Size2i &p_resolution, Error &r_error) {
r_error = OK; // Always succeeds for now.
// Ensure the canvas ID.
godot_js_config_canvas_id_get(canvas_id, 256);
// Handle contextmenu, webglcontextlost
- godot_js_display_setup_canvas(p_resolution.x, p_resolution.y, p_mode == WINDOW_MODE_FULLSCREEN, OS::get_singleton()->is_hidpi_allowed() ? 1 : 0);
+ godot_js_display_setup_canvas(p_resolution.x, p_resolution.y, p_window_mode == WINDOW_MODE_FULLSCREEN, OS::get_singleton()->is_hidpi_allowed() ? 1 : 0);
// Check if it's windows.
swap_cancel_ok = godot_js_display_is_swap_ok_cancel() == 1;
diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h
index ece38f1a95..bf5e229c9a 100644
--- a/platform/javascript/display_server_javascript.h
+++ b/platform/javascript/display_server_javascript.h
@@ -89,7 +89,7 @@ private:
void process_joypads();
static Vector<String> get_rendering_drivers_func();
- static DisplayServer *create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error);
+ static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error);
static void _dispatch_input_event(const Ref<InputEvent> &p_event);
@@ -109,99 +109,98 @@ public:
bool check_size_force_redraw();
// from DisplayServer
- void alert(const String &p_alert, const String &p_title = "ALERT!") override;
- bool has_feature(Feature p_feature) const override;
- String get_name() const override;
+ virtual bool has_feature(Feature p_feature) const override;
+ virtual String get_name() const override;
// cursor
- void cursor_set_shape(CursorShape p_shape) override;
- CursorShape cursor_get_shape() const override;
- void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override;
+ virtual void cursor_set_shape(CursorShape p_shape) override;
+ virtual CursorShape cursor_get_shape() const override;
+ virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override;
// mouse
- void mouse_set_mode(MouseMode p_mode) override;
- MouseMode mouse_get_mode() const override;
+ virtual void mouse_set_mode(MouseMode p_mode) override;
+ virtual MouseMode mouse_get_mode() const override;
// touch
- bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+ virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
// clipboard
- void clipboard_set(const String &p_text) override;
- String clipboard_get() const override;
+ virtual void clipboard_set(const String &p_text) override;
+ virtual String clipboard_get() const override;
// screen
- int get_screen_count() const override;
- Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
- Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
- Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
- int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
- float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+ virtual int get_screen_count() const override;
+ virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+ virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+ virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+ virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+ virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
- void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override;
- void virtual_keyboard_hide() override;
+ virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override;
+ virtual void virtual_keyboard_hide() override;
// windows
- Vector<DisplayServer::WindowID> get_window_list() const override;
- WindowID get_window_at_screen_position(const Point2i &p_position) const override;
+ virtual Vector<DisplayServer::WindowID> get_window_list() const override;
+ virtual WindowID get_window_at_screen_position(const Point2i &p_position) const override;
- void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override;
- ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override;
- void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
- void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
- void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
- void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
- void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
- void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override;
- int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override;
- void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID) override;
- Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const override;
- void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID) override;
- void window_set_transient(WindowID p_window, WindowID p_parent) override;
+ virtual void window_set_transient(WindowID p_window, WindowID p_parent) override;
- void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
- Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const override;
- void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
- Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const override;
- void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
- Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override;
- Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const override;
- void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) override;
- WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const override;
- bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const override;
- void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID) override;
- bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const override;
- void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override;
- void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override;
+ virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override;
- bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override;
- bool can_any_window_draw() const override;
+ virtual bool can_any_window_draw() const override;
// events
- void process_events() override;
+ virtual void process_events() override;
// icon
- void set_icon(const Ref<Image> &p_icon) override;
+ virtual void set_icon(const Ref<Image> &p_icon) override;
// others
- bool get_swap_cancel_ok() override;
- void swap_buffers() override;
+ virtual bool get_swap_cancel_ok() override;
+ virtual void swap_buffers() override;
static void register_javascript_driver();
- DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error);
+ DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Size2i &p_resolution, Error &r_error);
~DisplayServerJavaScript();
};
diff --git a/platform/javascript/dom_keys.inc b/platform/javascript/dom_keys.inc
index 69340ff58c..0e62776923 100644
--- a/platform/javascript/dom_keys.inc
+++ b/platform/javascript/dom_keys.inc
@@ -31,7 +31,7 @@
#include "core/os/keyboard.h"
// See https://w3c.github.io/uievents-code/#code-value-tables
-int dom_code2godot_scancode(EM_UTF8 const p_code[32], EM_UTF8 const p_key[32], bool p_physical) {
+Key dom_code2godot_scancode(EM_UTF8 const p_code[32], EM_UTF8 const p_key[32], bool p_physical) {
#define DOM2GODOT(p_str, p_godot_code) \
if (memcmp((const void *)p_str, (void *)p_code, strlen(p_str) + 1) == 0) { \
return KEY_##p_godot_code; \
@@ -79,7 +79,7 @@ int dom_code2godot_scancode(EM_UTF8 const p_code[32], EM_UTF8 const p_key[32], b
if (b0 > 0x60 && b0 < 0x7B) { // Lowercase ASCII.
b0 -= 32;
}
- return b0;
+ return (Key)b0;
}
#define _U_2BYTES_MASK 0xE0
@@ -88,11 +88,11 @@ int dom_code2godot_scancode(EM_UTF8 const p_code[32], EM_UTF8 const p_key[32], b
if (b2 == 0 && (b0 & _U_2BYTES_MASK) == _U_2BYTES) { // 2-bytes utf8, only known latin.
uint32_t key = ((b0 & ~_U_2BYTES_MASK) << 6) | (b1 & 0x3F);
if (key >= 0xA0 && key <= 0xDF) {
- return key;
+ return (Key)key;
}
if (key >= 0xE0 && key <= 0xFF) { // Lowercase known latin.
key -= 0x20;
- return key;
+ return (Key)key;
}
}
#undef _U_2BYTES_MASK
diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py
index ab98838e20..4dad2d5204 100644
--- a/platform/javascript/emscripten_helpers.py
+++ b/platform/javascript/emscripten_helpers.py
@@ -24,7 +24,10 @@ def get_build_version():
v = "%d.%d" % (version.major, version.minor)
if version.patch > 0:
v += ".%d" % version.patch
- v += ".%s.%s" % (version.status, name)
+ status = version.status
+ if os.getenv("GODOT_VERSION_STATUS") != None:
+ status = str(os.getenv("GODOT_VERSION_STATUS"))
+ v += ".%s.%s" % (status, name)
return v
diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp
index 7e49feee61..889b0bbd02 100644
--- a/platform/javascript/export/export.cpp
+++ b/platform/javascript/export/export.cpp
@@ -28,967 +28,9 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#include "core/io/image_loader.h"
-#include "core/io/json.h"
-#include "core/io/stream_peer_ssl.h"
-#include "core/io/tcp_server.h"
-#include "core/io/zip_io.h"
-#include "editor/editor_export.h"
-#include "editor/editor_node.h"
-#include "main/splash.gen.h"
-#include "platform/javascript/logo.gen.h"
-#include "platform/javascript/run_icon.gen.h"
+#include "export.h"
-class EditorHTTPServer : public RefCounted {
-private:
- Ref<TCPServer> server;
- Map<String, String> mimes;
- Ref<StreamPeerTCP> tcp;
- Ref<StreamPeerSSL> ssl;
- Ref<StreamPeer> peer;
- Ref<CryptoKey> key;
- Ref<X509Certificate> cert;
- bool use_ssl = false;
- uint64_t time = 0;
- uint8_t req_buf[4096];
- int req_pos = 0;
-
- void _clear_client() {
- peer = Ref<StreamPeer>();
- ssl = Ref<StreamPeerSSL>();
- tcp = Ref<StreamPeerTCP>();
- memset(req_buf, 0, sizeof(req_buf));
- time = 0;
- req_pos = 0;
- }
-
- void _set_internal_certs(Ref<Crypto> p_crypto) {
- const String cache_path = EditorPaths::get_singleton()->get_cache_dir();
- const String key_path = cache_path.plus_file("html5_server.key");
- const String crt_path = cache_path.plus_file("html5_server.crt");
- bool regen = !FileAccess::exists(key_path) || !FileAccess::exists(crt_path);
- if (!regen) {
- key = Ref<CryptoKey>(CryptoKey::create());
- cert = Ref<X509Certificate>(X509Certificate::create());
- if (key->load(key_path) != OK || cert->load(crt_path) != OK) {
- regen = true;
- }
- }
- if (regen) {
- key = p_crypto->generate_rsa(2048);
- key->save(key_path);
- cert = p_crypto->generate_self_signed_certificate(key, "CN=godot-debug.local,O=A Game Dev,C=XXA", "20140101000000", "20340101000000");
- cert->save(crt_path);
- }
- }
-
-public:
- EditorHTTPServer() {
- mimes["html"] = "text/html";
- mimes["js"] = "application/javascript";
- mimes["json"] = "application/json";
- mimes["pck"] = "application/octet-stream";
- mimes["png"] = "image/png";
- mimes["svg"] = "image/svg";
- mimes["wasm"] = "application/wasm";
- server.instance();
- stop();
- }
-
- void stop() {
- server->stop();
- _clear_client();
- }
-
- Error listen(int p_port, IPAddress p_address, bool p_use_ssl, String p_ssl_key, String p_ssl_cert) {
- use_ssl = p_use_ssl;
- if (use_ssl) {
- Ref<Crypto> crypto = Crypto::create();
- if (crypto.is_null()) {
- return ERR_UNAVAILABLE;
- }
- if (!p_ssl_key.is_empty() && !p_ssl_cert.is_empty()) {
- key = Ref<CryptoKey>(CryptoKey::create());
- Error err = key->load(p_ssl_key);
- ERR_FAIL_COND_V(err != OK, err);
- cert = Ref<X509Certificate>(X509Certificate::create());
- err = cert->load(p_ssl_cert);
- ERR_FAIL_COND_V(err != OK, err);
- } else {
- _set_internal_certs(crypto);
- }
- }
- return server->listen(p_port, p_address);
- }
-
- bool is_listening() const {
- return server->is_listening();
- }
-
- void _send_response() {
- Vector<String> psa = String((char *)req_buf).split("\r\n");
- int len = psa.size();
- ERR_FAIL_COND_MSG(len < 4, "Not enough response headers, got: " + itos(len) + ", expected >= 4.");
-
- Vector<String> req = psa[0].split(" ", false);
- ERR_FAIL_COND_MSG(req.size() < 2, "Invalid protocol or status code.");
-
- // Wrong protocol
- ERR_FAIL_COND_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", "Invalid method or HTTP version.");
-
- const String req_file = req[1].get_file();
- const String req_ext = req[1].get_extension();
- const String cache_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("web");
- const String filepath = cache_path.plus_file(req_file);
-
- if (!mimes.has(req_ext) || !FileAccess::exists(filepath)) {
- String s = "HTTP/1.1 404 Not Found\r\n";
- s += "Connection: Close\r\n";
- s += "\r\n";
- CharString cs = s.utf8();
- peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
- return;
- }
- const String ctype = mimes[req_ext];
-
- FileAccess *f = FileAccess::open(filepath, FileAccess::READ);
- ERR_FAIL_COND(!f);
- String s = "HTTP/1.1 200 OK\r\n";
- s += "Connection: Close\r\n";
- s += "Content-Type: " + ctype + "\r\n";
- s += "Access-Control-Allow-Origin: *\r\n";
- s += "Cross-Origin-Opener-Policy: same-origin\r\n";
- s += "Cross-Origin-Embedder-Policy: require-corp\r\n";
- s += "Cache-Control: no-store, max-age=0\r\n";
- s += "\r\n";
- CharString cs = s.utf8();
- Error err = peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
- if (err != OK) {
- memdelete(f);
- ERR_FAIL();
- }
-
- while (true) {
- uint8_t bytes[4096];
- uint64_t read = f->get_buffer(bytes, 4096);
- if (read == 0) {
- break;
- }
- err = peer->put_data(bytes, read);
- if (err != OK) {
- memdelete(f);
- ERR_FAIL();
- }
- }
- memdelete(f);
- }
-
- void poll() {
- if (!server->is_listening()) {
- return;
- }
- if (tcp.is_null()) {
- if (!server->is_connection_available()) {
- return;
- }
- tcp = server->take_connection();
- peer = tcp;
- time = OS::get_singleton()->get_ticks_usec();
- }
- if (OS::get_singleton()->get_ticks_usec() - time > 1000000) {
- _clear_client();
- return;
- }
- if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) {
- return;
- }
-
- if (use_ssl) {
- if (ssl.is_null()) {
- ssl = Ref<StreamPeerSSL>(StreamPeerSSL::create());
- peer = ssl;
- ssl->set_blocking_handshake_enabled(false);
- if (ssl->accept_stream(tcp, key, cert) != OK) {
- _clear_client();
- return;
- }
- }
- ssl->poll();
- if (ssl->get_status() == StreamPeerSSL::STATUS_HANDSHAKING) {
- // Still handshaking, keep waiting.
- return;
- }
- if (ssl->get_status() != StreamPeerSSL::STATUS_CONNECTED) {
- _clear_client();
- return;
- }
- }
-
- while (true) {
- char *r = (char *)req_buf;
- int l = req_pos - 1;
- if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') {
- _send_response();
- _clear_client();
- return;
- }
-
- int read = 0;
- ERR_FAIL_COND(req_pos >= 4096);
- Error err = peer->get_partial_data(&req_buf[req_pos], 1, read);
- if (err != OK) {
- // Got an error
- _clear_client();
- return;
- } else if (read != 1) {
- // Busy, wait next poll
- return;
- }
- req_pos += read;
- }
- }
-};
-
-class EditorExportPlatformJavaScript : public EditorExportPlatform {
- GDCLASS(EditorExportPlatformJavaScript, EditorExportPlatform);
-
- Ref<ImageTexture> logo;
- Ref<ImageTexture> run_icon;
- Ref<ImageTexture> stop_icon;
- int menu_options = 0;
-
- Ref<EditorHTTPServer> server;
- bool server_quit = false;
- Mutex server_lock;
- 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;
- }
-
- Ref<Image> _get_project_icon() const {
- Ref<Image> icon;
- icon.instance();
- const String icon_path = String(GLOBAL_GET("application/config/icon")).strip_edges();
- if (icon_path.is_empty() || ImageLoader::load_image(icon_path, icon) != OK) {
- return EditorNode::get_singleton()->get_editor_theme()->get_icon("DefaultProjectIcon", "EditorIcons")->get_image();
- }
- return icon;
- }
-
- Ref<Image> _get_project_splash() const {
- Ref<Image> splash;
- splash.instance();
- const String splash_path = String(GLOBAL_GET("application/boot_splash/image")).strip_edges();
- if (splash_path.is_empty() || ImageLoader::load_image(splash_path, splash) != OK) {
- return Ref<Image>(memnew(Image(boot_splash_png)));
- }
- return splash;
- }
-
- Error _extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa);
- void _replace_strings(Map<String, String> p_replaces, Vector<uint8_t> &r_template);
- 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, const Dictionary &p_file_sizes);
- Error _add_manifest_icon(const String &p_path, const String &p_icon, int p_size, Array &r_arr);
- Error _build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects);
- Error _write_or_error(const uint8_t *p_content, int p_len, String p_path);
-
- static void _server_thread_poll(void *data);
-
-public:
- virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) override;
-
- virtual void get_export_options(List<ExportOption> *r_options) override;
-
- virtual String get_name() const override;
- virtual String get_os_name() const override;
- virtual Ref<Texture2D> get_logo() const override;
-
- virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override;
- virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override;
- virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;
-
- virtual bool poll_export() override;
- virtual int get_options_count() const override;
- virtual String get_option_label(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); }
- virtual String get_option_tooltip(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); }
- virtual Ref<ImageTexture> get_option_icon(int p_index) const override;
- virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) override;
- virtual Ref<Texture2D> get_run_icon() const override;
-
- virtual void get_platform_features(List<String> *r_features) override {
- r_features->push_back("web");
- r_features->push_back(get_os_name());
- }
-
- virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override {
- }
-
- String get_debug_protocol() const override { return "ws://"; }
-
- EditorExportPlatformJavaScript();
- ~EditorExportPlatformJavaScript();
-};
-
-Error EditorExportPlatformJavaScript::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) {
- FileAccess *src_f = nullptr;
- zlib_filefunc_def io = zipio_create_io_from_file(&src_f);
- unzFile pkg = unzOpen2(p_template.utf8().get_data(), &io);
-
- if (!pkg) {
- EditorNode::get_singleton()->show_warning(TTR("Could not open template for export:") + "\n" + p_template);
- return ERR_FILE_NOT_FOUND;
- }
-
- if (unzGoToFirstFile(pkg) != UNZ_OK) {
- EditorNode::get_singleton()->show_warning(TTR("Invalid export template:") + "\n" + p_template);
- unzClose(pkg);
- return ERR_FILE_CORRUPT;
- }
-
- do {
- //get filename
- unz_file_info info;
- char fname[16384];
- unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
-
- String file = fname;
-
- // Skip service worker and offline page if not exporting pwa.
- if (!pwa && (file == "godot.service.worker.js" || file == "godot.offline.html")) {
- continue;
- }
- Vector<uint8_t> data;
- data.resize(info.uncompressed_size);
-
- //read
- unzOpenCurrentFile(pkg);
- unzReadCurrentFile(pkg, data.ptrw(), data.size());
- unzCloseCurrentFile(pkg);
-
- //write
- String dst = p_dir.plus_file(file.replace("godot", p_name));
- FileAccess *f = FileAccess::open(dst, FileAccess::WRITE);
- if (!f) {
- EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + dst);
- unzClose(pkg);
- return ERR_FILE_CANT_WRITE;
- }
- f->store_buffer(data.ptr(), data.size());
- memdelete(f);
-
- } while (unzGoToNextFile(pkg) == UNZ_OK);
- unzClose(pkg);
- return OK;
-}
-
-Error EditorExportPlatformJavaScript::_write_or_error(const uint8_t *p_content, int p_size, String p_path) {
- FileAccess *f = FileAccess::open(p_path, FileAccess::WRITE);
- if (!f) {
- EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + p_path);
- return ERR_FILE_CANT_WRITE;
- }
- f->store_buffer(p_content, p_size);
- memdelete(f);
- return OK;
-}
-
-void EditorExportPlatformJavaScript::_replace_strings(Map<String, String> p_replaces, Vector<uint8_t> &r_template) {
- String str_template = String::utf8(reinterpret_cast<const char *>(r_template.ptr()), r_template.size());
- String out;
- Vector<String> lines = str_template.split("\n");
- for (int i = 0; i < lines.size(); i++) {
- String current_line = lines[i];
- for (Map<String, String>::Element *E = p_replaces.front(); E; E = E->next()) {
- current_line = current_line.replace(E->key(), E->get());
- }
- out += current_line + "\n";
- }
- CharString cs = out.utf8();
- r_template.resize(cs.length());
- for (int i = 0; i < cs.length(); i++) {
- r_template.write[i] = cs[i];
- }
-}
-
-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, const Dictionary &p_file_sizes) {
- // Engine.js config
- Dictionary config;
- Array libs;
- for (int i = 0; i < p_shared_objects.size(); i++) {
- libs.push_back(p_shared_objects[i].path.get_file());
- }
- Vector<String> flags;
- gen_export_flags(flags, p_flags & (~DEBUG_FLAG_DUMB_CLIENT));
- Array args;
- for (int i = 0; i < flags.size(); i++) {
- args.push_back(flags[i]);
- }
- config["canvasResizePolicy"] = p_preset->get("html/canvas_resize_policy");
- config["experimentalVK"] = p_preset->get("html/experimental_virtual_keyboard");
- config["gdnativeLibs"] = libs;
- config["executable"] = p_name;
- config["args"] = args;
- config["fileSizes"] = p_file_sizes;
-
- String head_include;
- if (p_preset->get("html/export_icon")) {
- head_include += "<link id='-gd-engine-icon' rel='icon' type='image/png' href='" + p_name + ".icon.png' />\n";
- head_include += "<link rel='apple-touch-icon' href='" + p_name + ".apple-touch-icon.png'/>\n";
- }
- if (p_preset->get("progressive_web_app/enabled")) {
- head_include += "<link rel='manifest' href='" + p_name + ".manifest.json'>\n";
- head_include += "<script type='application/javascript'>window.addEventListener('load', () => {if ('serviceWorker' in navigator) {navigator.serviceWorker.register('" +
- p_name + ".service.worker.js');}});</script>\n";
- }
-
- // Replaces HTML string
- const String str_config = JSON::print(config);
- const String custom_head_include = p_preset->get("html/head_include");
- Map<String, String> replaces;
- replaces["$GODOT_URL"] = p_name + ".js";
- replaces["$GODOT_PROJECT_NAME"] = ProjectSettings::get_singleton()->get_setting("application/config/name");
- replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;
- replaces["$GODOT_CONFIG"] = str_config;
- _replace_strings(replaces, p_html);
-}
-
-Error EditorExportPlatformJavaScript::_add_manifest_icon(const String &p_path, const String &p_icon, int p_size, Array &r_arr) {
- const String name = p_path.get_file().get_basename();
- const String icon_name = vformat("%s.%dx%d.png", name, p_size, p_size);
- const String icon_dest = p_path.get_base_dir().plus_file(icon_name);
-
- Ref<Image> icon;
- if (!p_icon.is_empty()) {
- icon.instance();
- const Error err = ImageLoader::load_image(p_icon, icon);
- if (err != OK) {
- EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + p_icon);
- return err;
- }
- if (icon->get_width() != p_size || icon->get_height() != p_size) {
- icon->resize(p_size, p_size);
- }
- } else {
- icon = _get_project_icon();
- icon->resize(p_size, p_size);
- }
- const Error err = icon->save_png(icon_dest);
- if (err != OK) {
- EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + icon_dest);
- return err;
- }
- Dictionary icon_dict;
- icon_dict["sizes"] = vformat("%dx%d", p_size, p_size);
- icon_dict["type"] = "image/png";
- icon_dict["src"] = icon_name;
- r_arr.push_back(icon_dict);
- return err;
-}
-
-Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects) {
- // Service worker
- const String dir = p_path.get_base_dir();
- const String name = p_path.get_file().get_basename();
- const ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
- Map<String, String> replaces;
- replaces["@GODOT_VERSION@"] = "1";
- replaces["@GODOT_NAME@"] = name;
- replaces["@GODOT_OFFLINE_PAGE@"] = name + ".offline.html";
- Array files;
- replaces["@GODOT_OPT_CACHE@"] = JSON::print(files);
- files.push_back(name + ".html");
- files.push_back(name + ".js");
- files.push_back(name + ".wasm");
- files.push_back(name + ".pck");
- files.push_back(name + ".offline.html");
- if (p_preset->get("html/export_icon")) {
- files.push_back(name + ".icon.png");
- files.push_back(name + ".apple-touch-icon.png");
- }
- if (mode == EXPORT_MODE_THREADS) {
- files.push_back(name + ".worker.js");
- files.push_back(name + ".audio.worklet.js");
- } else if (mode == EXPORT_MODE_GDNATIVE) {
- files.push_back(name + ".side.wasm");
- for (int i = 0; i < p_shared_objects.size(); i++) {
- files.push_back(p_shared_objects[i].path.get_file());
- }
- }
- replaces["@GODOT_CACHE@"] = JSON::print(files);
-
- const String sw_path = dir.plus_file(name + ".service.worker.js");
- Vector<uint8_t> sw;
- {
- FileAccess *f = FileAccess::open(sw_path, FileAccess::READ);
- if (!f) {
- EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + sw_path);
- return ERR_FILE_CANT_READ;
- }
- sw.resize(f->get_length());
- f->get_buffer(sw.ptrw(), sw.size());
- memdelete(f);
- f = nullptr;
- }
- _replace_strings(replaces, sw);
- Error err = _write_or_error(sw.ptr(), sw.size(), dir.plus_file(name + ".service.worker.js"));
- if (err != OK) {
- return err;
- }
-
- // Custom offline page
- const String offline_page = p_preset->get("progressive_web_app/offline_page");
- if (!offline_page.is_empty()) {
- DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- const String offline_dest = dir.plus_file(name + ".offline.html");
- err = da->copy(ProjectSettings::get_singleton()->globalize_path(offline_page), offline_dest);
- if (err != OK) {
- EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + offline_dest);
- return err;
- }
- }
-
- // Manifest
- const char *modes[4] = { "fullscreen", "standalone", "minimal-ui", "browser" };
- const char *orientations[3] = { "any", "landscape", "portrait" };
- const int display = CLAMP(int(p_preset->get("progressive_web_app/display")), 0, 4);
- const int orientation = CLAMP(int(p_preset->get("progressive_web_app/orientation")), 0, 3);
-
- Dictionary manifest;
- String proj_name = ProjectSettings::get_singleton()->get_setting("application/config/name");
- if (proj_name.is_empty()) {
- proj_name = "Godot Game";
- }
- manifest["name"] = proj_name;
- manifest["start_url"] = "./" + name + ".html";
- manifest["display"] = String::utf8(modes[display]);
- manifest["orientation"] = String::utf8(orientations[orientation]);
- manifest["background_color"] = "#" + p_preset->get("progressive_web_app/background_color").operator Color().to_html(false);
-
- Array icons_arr;
- const String icon144_path = p_preset->get("progressive_web_app/icon_144x144");
- err = _add_manifest_icon(p_path, icon144_path, 144, icons_arr);
- if (err != OK) {
- return err;
- }
- const String icon180_path = p_preset->get("progressive_web_app/icon_180x180");
- err = _add_manifest_icon(p_path, icon180_path, 180, icons_arr);
- if (err != OK) {
- return err;
- }
- const String icon512_path = p_preset->get("progressive_web_app/icon_512x512");
- err = _add_manifest_icon(p_path, icon512_path, 512, icons_arr);
- if (err != OK) {
- return err;
- }
- manifest["icons"] = icons_arr;
-
- CharString cs = JSON::print(manifest).utf8();
- err = _write_or_error((const uint8_t *)cs.get_data(), cs.length(), dir.plus_file(name + ".manifest.json"));
- if (err != OK) {
- return err;
- }
-
- return OK;
-}
-
-void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) {
- if (p_preset->get("vram_texture_compression/for_desktop")) {
- r_features->push_back("s3tc");
- }
-
- if (p_preset->get("vram_texture_compression/for_mobile")) {
- String driver = ProjectSettings::get_singleton()->get("rendering/driver/driver_name");
- if (driver == "GLES2") {
- r_features->push_back("etc");
- } else if (driver == "Vulkan") {
- // FIXME: Review if this is correct.
- 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::BOOL, "html/export_icon"), true));
- 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::INT, "html/canvas_resize_policy", PROPERTY_HINT_ENUM, "None,Project,Adaptive"), 2));
- r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/experimental_virtual_keyboard"), false));
- r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/enabled"), false));
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/offline_page", PROPERTY_HINT_FILE, "*.html"), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/display", PROPERTY_HINT_ENUM, "Fullscreen,Standalone,Minimal UI,Browser"), 1));
- r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/orientation", PROPERTY_HINT_ENUM, "Any,Landscape,Portrait"), 0));
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_144x144", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg,*.svgz"), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_180x180", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg,*.svgz"), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_512x512", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg,*.svgz"), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "progressive_web_app/background_color", PROPERTY_HINT_COLOR_NO_ALPHA), Color()));
-}
-
-String EditorExportPlatformJavaScript::get_name() const {
- return "HTML5";
-}
-
-String EditorExportPlatformJavaScript::get_os_name() const {
- return "HTML5";
-}
-
-Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const {
- return logo;
-}
-
-bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const {
- String err;
- bool valid = false;
- ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
-
- // Look for export templates (first official, and if defined custom templates).
- bool dvalid = exists_export_template(_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"));
- if (!dvalid) {
- err += TTR("Custom debug template not found.") + "\n";
- }
- }
- if (p_preset->get("custom_template/release") != "") {
- rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
- if (!rvalid) {
- err += TTR("Custom release template not found.") + "\n";
- }
- }
-
- valid = dvalid || rvalid;
- r_missing_templates = !valid;
-
- // Validate the rest of the configuration.
-
- if (p_preset->get("vram_texture_compression/for_mobile")) {
- String etc_error = test_etc2();
- if (etc_error != String()) {
- valid = false;
- err += etc_error;
- }
- }
-
- if (!err.is_empty()) {
- r_error = err;
- }
-
- return valid;
-}
-
-List<String> EditorExportPlatformJavaScript::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
- List<String> list;
- list.push_back("html");
- return list;
-}
-
-Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
- ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
-
- const String custom_debug = p_preset->get("custom_template/debug");
- const String custom_release = p_preset->get("custom_template/release");
- const String custom_html = p_preset->get("html/custom_html_shell");
- const bool export_icon = p_preset->get("html/export_icon");
- const bool pwa = p_preset->get("progressive_web_app/enabled");
-
- const String base_dir = p_path.get_base_dir();
- const String base_path = p_path.get_basename();
- const String base_name = p_path.get_file().get_basename();
-
- // Find the correct template
- String template_path = p_debug ? custom_debug : custom_release;
- template_path = template_path.strip_edges();
- if (template_path == String()) {
- ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
- template_path = find_export_template(_get_template_name(mode, p_debug));
- }
-
- if (!DirAccess::exists(base_dir)) {
- return ERR_FILE_BAD_PATH;
- }
-
- if (template_path != String() && !FileAccess::exists(template_path)) {
- EditorNode::get_singleton()->show_warning(TTR("Template file not found:") + "\n" + template_path);
- return ERR_FILE_NOT_FOUND;
- }
-
- // Export pck and shared objects
- Vector<SharedObject> shared_objects;
- String pck_path = base_path + ".pck";
- 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 = 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);
- da = nullptr;
-
- // Extract templates.
- error = _extract_template(template_path, base_dir, base_name, pwa);
- if (error) {
- return error;
- }
-
- // Parse generated file sizes (pck and wasm, to help show a meaningful loading bar).
- Dictionary file_sizes;
- FileAccess *f = nullptr;
- f = FileAccess::open(pck_path, FileAccess::READ);
- if (f) {
- file_sizes[pck_path.get_file()] = (uint64_t)f->get_length();
- memdelete(f);
- f = nullptr;
- }
- f = FileAccess::open(base_path + ".wasm", FileAccess::READ);
- if (f) {
- file_sizes[base_name + ".wasm"] = (uint64_t)f->get_length();
- memdelete(f);
- f = nullptr;
- }
-
- // Read the HTML shell file (custom or from template).
- const String html_path = custom_html.is_empty() ? base_path + ".html" : custom_html;
- Vector<uint8_t> html;
- f = FileAccess::open(html_path, FileAccess::READ);
- if (!f) {
- EditorNode::get_singleton()->show_warning(TTR("Could not read HTML shell:") + "\n" + html_path);
- return ERR_FILE_CANT_READ;
- }
- html.resize(f->get_length());
- f->get_buffer(html.ptrw(), html.size());
- memdelete(f);
- f = nullptr;
-
- // Generate HTML file with replaced strings.
- _fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes);
- Error err = _write_or_error(html.ptr(), html.size(), p_path);
- if (err != OK) {
- return err;
- }
- html.resize(0);
-
- // Export splash (why?)
- Ref<Image> splash = _get_project_splash();
- const String splash_png_path = base_path + ".png";
- if (splash->save_png(splash_png_path) != OK) {
- EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + splash_png_path);
- return ERR_FILE_CANT_WRITE;
- }
-
- // Save a favicon that can be accessed without waiting for the project to finish loading.
- // This way, the favicon can be displayed immediately when loading the page.
- if (export_icon) {
- Ref<Image> favicon = _get_project_icon();
- const String favicon_png_path = base_path + ".icon.png";
- if (favicon->save_png(favicon_png_path) != OK) {
- EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + favicon_png_path);
- return ERR_FILE_CANT_WRITE;
- }
- favicon->resize(180, 180);
- const String apple_icon_png_path = base_path + ".apple-touch-icon.png";
- if (favicon->save_png(apple_icon_png_path) != OK) {
- EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + apple_icon_png_path);
- return ERR_FILE_CANT_WRITE;
- }
- }
-
- // Generate the PWA worker and manifest
- if (pwa) {
- err = _build_pwa(p_preset, p_path, shared_objects);
- if (err != OK) {
- return err;
- }
- }
-
- return OK;
-}
-
-bool EditorExportPlatformJavaScript::poll_export() {
- Ref<EditorExportPreset> preset;
-
- for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
- Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);
- if (ep->is_runnable() && ep->get_platform() == this) {
- preset = ep;
- break;
- }
- }
-
- int prev = menu_options;
- menu_options = preset.is_valid();
- if (server->is_listening()) {
- if (menu_options == 0) {
- MutexLock lock(server_lock);
- server->stop();
- } else {
- menu_options += 1;
- }
- }
- return menu_options != prev;
-}
-
-Ref<ImageTexture> EditorExportPlatformJavaScript::get_option_icon(int p_index) const {
- return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
-}
-
-int EditorExportPlatformJavaScript::get_options_count() const {
- return menu_options;
-}
-
-Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) {
- if (p_option == 1) {
- MutexLock lock(server_lock);
- server->stop();
- return OK;
- }
-
- const String dest = EditorPaths::get_singleton()->get_cache_dir().plus_file("web");
- DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- if (!da->dir_exists(dest)) {
- Error err = da->make_dir_recursive(dest);
- if (err != OK) {
- EditorNode::get_singleton()->show_warning(TTR("Could not create HTTP server directory:") + "\n" + dest);
- return err;
- }
- }
- const String basepath = dest.plus_file("tmp_js_export");
- Error err = export_project(p_preset, true, basepath + ".html", p_debug_flags);
- if (err != OK) {
- // Export generates several files, clean them up on failure.
- DirAccess::remove_file_or_error(basepath + ".html");
- DirAccess::remove_file_or_error(basepath + ".offline.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 + ".service.worker.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(basepath + ".icon.png");
- DirAccess::remove_file_or_error(basepath + ".apple-touch-icon.png");
- return err;
- }
-
- const uint16_t bind_port = EDITOR_GET("export/web/http_port");
- // Resolve host if needed.
- const String bind_host = EDITOR_GET("export/web/http_host");
- IPAddress bind_ip;
- if (bind_host.is_valid_ip_address()) {
- bind_ip = bind_host;
- } else {
- bind_ip = IP::get_singleton()->resolve_hostname(bind_host);
- }
- ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + bind_host + "'. Try using '127.0.0.1'.");
-
- const bool use_ssl = EDITOR_GET("export/web/use_ssl");
- const String ssl_key = EDITOR_GET("export/web/ssl_key");
- const String ssl_cert = EDITOR_GET("export/web/ssl_certificate");
-
- // Restart server.
- {
- MutexLock lock(server_lock);
-
- server->stop();
- err = server->listen(bind_port, bind_ip, use_ssl, ssl_key, ssl_cert);
- }
- if (err != OK) {
- EditorNode::get_singleton()->show_warning(TTR("Error starting HTTP server:") + "\n" + itos(err));
- return err;
- }
-
- OS::get_singleton()->shell_open(String((use_ssl ? "https://" : "http://") + bind_host + ":" + itos(bind_port) + "/tmp_js_export.html"));
- // FIXME: Find out how to clean up export files after running the successfully
- // exported game. Might not be trivial.
- return OK;
-}
-
-Ref<Texture2D> EditorExportPlatformJavaScript::get_run_icon() const {
- return run_icon;
-}
-
-void EditorExportPlatformJavaScript::_server_thread_poll(void *data) {
- EditorExportPlatformJavaScript *ej = (EditorExportPlatformJavaScript *)data;
- while (!ej->server_quit) {
- OS::get_singleton()->delay_usec(1000);
- {
- MutexLock lock(ej->server_lock);
- ej->server->poll();
- }
- }
-}
-
-EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() {
- server.instance();
- server_thread.start(_server_thread_poll, this);
-
- Ref<Image> img = memnew(Image(_javascript_logo));
- logo.instance();
- logo->create_from_image(img);
-
- img = Ref<Image>(memnew(Image(_javascript_run_icon)));
- run_icon.instance();
- run_icon->create_from_image(img);
-
- Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
- if (theme.is_valid()) {
- stop_icon = theme->get_icon("Stop", "EditorIcons");
- } else {
- stop_icon.instance();
- }
-}
-
-EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() {
- server->stop();
- server_quit = true;
- server_thread.wait_to_finish();
-}
+#include "export_plugin.h"
void register_javascript_exporter() {
EDITOR_DEF("export/web/http_host", "localhost");
@@ -1001,6 +43,6 @@ void register_javascript_exporter() {
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/ssl_certificate", PROPERTY_HINT_GLOBAL_FILE, "*.crt,*.pem"));
Ref<EditorExportPlatformJavaScript> platform;
- platform.instance();
+ platform.instantiate();
EditorExport::get_singleton()->add_export_platform(platform);
}
diff --git a/platform/javascript/export/export.h b/platform/javascript/export/export.h
index e641339f55..8e8161b547 100644
--- a/platform/javascript/export/export.h
+++ b/platform/javascript/export/export.h
@@ -28,4 +28,9 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
+#ifndef JAVASCRIPT_EXPORT_H
+#define JAVASCRIPT_EXPORT_H
+
void register_javascript_exporter();
+
+#endif
diff --git a/platform/javascript/export/export_plugin.cpp b/platform/javascript/export/export_plugin.cpp
new file mode 100644
index 0000000000..5d285a6e60
--- /dev/null
+++ b/platform/javascript/export/export_plugin.cpp
@@ -0,0 +1,671 @@
+/*************************************************************************/
+/* export_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. */
+/*************************************************************************/
+
+#include "export_plugin.h"
+
+Error EditorExportPlatformJavaScript::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) {
+ FileAccess *src_f = nullptr;
+ zlib_filefunc_def io = zipio_create_io_from_file(&src_f);
+ unzFile pkg = unzOpen2(p_template.utf8().get_data(), &io);
+
+ if (!pkg) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not open template for export:") + "\n" + p_template);
+ return ERR_FILE_NOT_FOUND;
+ }
+
+ if (unzGoToFirstFile(pkg) != UNZ_OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Invalid export template:") + "\n" + p_template);
+ unzClose(pkg);
+ return ERR_FILE_CORRUPT;
+ }
+
+ do {
+ //get filename
+ unz_file_info info;
+ char fname[16384];
+ unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
+
+ String file = fname;
+
+ // Skip service worker and offline page if not exporting pwa.
+ if (!pwa && (file == "godot.service.worker.js" || file == "godot.offline.html")) {
+ continue;
+ }
+ Vector<uint8_t> data;
+ data.resize(info.uncompressed_size);
+
+ //read
+ unzOpenCurrentFile(pkg);
+ unzReadCurrentFile(pkg, data.ptrw(), data.size());
+ unzCloseCurrentFile(pkg);
+
+ //write
+ String dst = p_dir.plus_file(file.replace("godot", p_name));
+ FileAccess *f = FileAccess::open(dst, FileAccess::WRITE);
+ if (!f) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + dst);
+ unzClose(pkg);
+ return ERR_FILE_CANT_WRITE;
+ }
+ f->store_buffer(data.ptr(), data.size());
+ memdelete(f);
+
+ } while (unzGoToNextFile(pkg) == UNZ_OK);
+ unzClose(pkg);
+ return OK;
+}
+
+Error EditorExportPlatformJavaScript::_write_or_error(const uint8_t *p_content, int p_size, String p_path) {
+ FileAccess *f = FileAccess::open(p_path, FileAccess::WRITE);
+ if (!f) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + p_path);
+ return ERR_FILE_CANT_WRITE;
+ }
+ f->store_buffer(p_content, p_size);
+ memdelete(f);
+ return OK;
+}
+
+void EditorExportPlatformJavaScript::_replace_strings(Map<String, String> p_replaces, Vector<uint8_t> &r_template) {
+ String str_template = String::utf8(reinterpret_cast<const char *>(r_template.ptr()), r_template.size());
+ String out;
+ Vector<String> lines = str_template.split("\n");
+ for (int i = 0; i < lines.size(); i++) {
+ String current_line = lines[i];
+ for (Map<String, String>::Element *E = p_replaces.front(); E; E = E->next()) {
+ current_line = current_line.replace(E->key(), E->get());
+ }
+ out += current_line + "\n";
+ }
+ CharString cs = out.utf8();
+ r_template.resize(cs.length());
+ for (int i = 0; i < cs.length(); i++) {
+ r_template.write[i] = cs[i];
+ }
+}
+
+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, const Dictionary &p_file_sizes) {
+ // Engine.js config
+ Dictionary config;
+ Array libs;
+ for (int i = 0; i < p_shared_objects.size(); i++) {
+ libs.push_back(p_shared_objects[i].path.get_file());
+ }
+ Vector<String> flags;
+ gen_export_flags(flags, p_flags & (~DEBUG_FLAG_DUMB_CLIENT));
+ Array args;
+ for (int i = 0; i < flags.size(); i++) {
+ args.push_back(flags[i]);
+ }
+ config["canvasResizePolicy"] = p_preset->get("html/canvas_resize_policy");
+ config["experimentalVK"] = p_preset->get("html/experimental_virtual_keyboard");
+ config["focusCanvas"] = p_preset->get("html/focus_canvas_on_start");
+ config["gdnativeLibs"] = libs;
+ config["executable"] = p_name;
+ config["args"] = args;
+ config["fileSizes"] = p_file_sizes;
+
+ String head_include;
+ if (p_preset->get("html/export_icon")) {
+ head_include += "<link id='-gd-engine-icon' rel='icon' type='image/png' href='" + p_name + ".icon.png' />\n";
+ head_include += "<link rel='apple-touch-icon' href='" + p_name + ".apple-touch-icon.png'/>\n";
+ }
+ if (p_preset->get("progressive_web_app/enabled")) {
+ head_include += "<link rel='manifest' href='" + p_name + ".manifest.json'>\n";
+ head_include += "<script type='application/javascript'>window.addEventListener('load', () => {if ('serviceWorker' in navigator) {navigator.serviceWorker.register('" +
+ p_name + ".service.worker.js');}});</script>\n";
+ }
+
+ // Replaces HTML string
+ const String str_config = Variant(config).to_json_string();
+ const String custom_head_include = p_preset->get("html/head_include");
+ Map<String, String> replaces;
+ replaces["$GODOT_URL"] = p_name + ".js";
+ replaces["$GODOT_PROJECT_NAME"] = ProjectSettings::get_singleton()->get_setting("application/config/name");
+ replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;
+ replaces["$GODOT_CONFIG"] = str_config;
+ _replace_strings(replaces, p_html);
+}
+
+Error EditorExportPlatformJavaScript::_add_manifest_icon(const String &p_path, const String &p_icon, int p_size, Array &r_arr) {
+ const String name = p_path.get_file().get_basename();
+ const String icon_name = vformat("%s.%dx%d.png", name, p_size, p_size);
+ const String icon_dest = p_path.get_base_dir().plus_file(icon_name);
+
+ Ref<Image> icon;
+ if (!p_icon.is_empty()) {
+ icon.instantiate();
+ const Error err = ImageLoader::load_image(p_icon, icon);
+ if (err != OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + p_icon);
+ return err;
+ }
+ if (icon->get_width() != p_size || icon->get_height() != p_size) {
+ icon->resize(p_size, p_size);
+ }
+ } else {
+ icon = _get_project_icon();
+ icon->resize(p_size, p_size);
+ }
+ const Error err = icon->save_png(icon_dest);
+ if (err != OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + icon_dest);
+ return err;
+ }
+ Dictionary icon_dict;
+ icon_dict["sizes"] = vformat("%dx%d", p_size, p_size);
+ icon_dict["type"] = "image/png";
+ icon_dict["src"] = icon_name;
+ r_arr.push_back(icon_dict);
+ return err;
+}
+
+Error EditorExportPlatformJavaScript::_build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects) {
+ // Service worker
+ const String dir = p_path.get_base_dir();
+ const String name = p_path.get_file().get_basename();
+ const ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
+ Map<String, String> replaces;
+ replaces["@GODOT_VERSION@"] = "1";
+ replaces["@GODOT_NAME@"] = name;
+ replaces["@GODOT_OFFLINE_PAGE@"] = name + ".offline.html";
+ Array files;
+ replaces["@GODOT_OPT_CACHE@"] = Variant(files).to_json_string();
+ files.push_back(name + ".html");
+ files.push_back(name + ".js");
+ files.push_back(name + ".wasm");
+ files.push_back(name + ".pck");
+ files.push_back(name + ".offline.html");
+ if (p_preset->get("html/export_icon")) {
+ files.push_back(name + ".icon.png");
+ files.push_back(name + ".apple-touch-icon.png");
+ }
+ if (mode == EXPORT_MODE_THREADS) {
+ files.push_back(name + ".worker.js");
+ files.push_back(name + ".audio.worklet.js");
+ } else if (mode == EXPORT_MODE_GDNATIVE) {
+ files.push_back(name + ".side.wasm");
+ for (int i = 0; i < p_shared_objects.size(); i++) {
+ files.push_back(p_shared_objects[i].path.get_file());
+ }
+ }
+ replaces["@GODOT_CACHE@"] = Variant(files).to_json_string();
+
+ const String sw_path = dir.plus_file(name + ".service.worker.js");
+ Vector<uint8_t> sw;
+ {
+ FileAccess *f = FileAccess::open(sw_path, FileAccess::READ);
+ if (!f) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + sw_path);
+ return ERR_FILE_CANT_READ;
+ }
+ sw.resize(f->get_length());
+ f->get_buffer(sw.ptrw(), sw.size());
+ memdelete(f);
+ f = nullptr;
+ }
+ _replace_strings(replaces, sw);
+ Error err = _write_or_error(sw.ptr(), sw.size(), dir.plus_file(name + ".service.worker.js"));
+ if (err != OK) {
+ return err;
+ }
+
+ // Custom offline page
+ const String offline_page = p_preset->get("progressive_web_app/offline_page");
+ if (!offline_page.is_empty()) {
+ DirAccess *da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ const String offline_dest = dir.plus_file(name + ".offline.html");
+ err = da->copy(ProjectSettings::get_singleton()->globalize_path(offline_page), offline_dest);
+ if (err != OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not read file:") + "\n" + offline_dest);
+ return err;
+ }
+ }
+
+ // Manifest
+ const char *modes[4] = { "fullscreen", "standalone", "minimal-ui", "browser" };
+ const char *orientations[3] = { "any", "landscape", "portrait" };
+ const int display = CLAMP(int(p_preset->get("progressive_web_app/display")), 0, 4);
+ const int orientation = CLAMP(int(p_preset->get("progressive_web_app/orientation")), 0, 3);
+
+ Dictionary manifest;
+ String proj_name = ProjectSettings::get_singleton()->get_setting("application/config/name");
+ if (proj_name.is_empty()) {
+ proj_name = "Godot Game";
+ }
+ manifest["name"] = proj_name;
+ manifest["start_url"] = "./" + name + ".html";
+ manifest["display"] = String::utf8(modes[display]);
+ manifest["orientation"] = String::utf8(orientations[orientation]);
+ manifest["background_color"] = "#" + p_preset->get("progressive_web_app/background_color").operator Color().to_html(false);
+
+ Array icons_arr;
+ const String icon144_path = p_preset->get("progressive_web_app/icon_144x144");
+ err = _add_manifest_icon(p_path, icon144_path, 144, icons_arr);
+ if (err != OK) {
+ return err;
+ }
+ const String icon180_path = p_preset->get("progressive_web_app/icon_180x180");
+ err = _add_manifest_icon(p_path, icon180_path, 180, icons_arr);
+ if (err != OK) {
+ return err;
+ }
+ const String icon512_path = p_preset->get("progressive_web_app/icon_512x512");
+ err = _add_manifest_icon(p_path, icon512_path, 512, icons_arr);
+ if (err != OK) {
+ return err;
+ }
+ manifest["icons"] = icons_arr;
+
+ CharString cs = Variant(manifest).to_json_string().utf8();
+ err = _write_or_error((const uint8_t *)cs.get_data(), cs.length(), dir.plus_file(name + ".manifest.json"));
+ if (err != OK) {
+ return err;
+ }
+
+ return OK;
+}
+
+void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) {
+ if (p_preset->get("vram_texture_compression/for_desktop")) {
+ r_features->push_back("s3tc");
+ }
+
+ if (p_preset->get("vram_texture_compression/for_mobile")) {
+ String driver = ProjectSettings::get_singleton()->get("rendering/driver/driver_name");
+ if (driver == "GLES2") {
+ r_features->push_back("etc");
+ } else if (driver == "Vulkan") {
+ // FIXME: Review if this is correct.
+ 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::BOOL, "html/export_icon"), true));
+ 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::INT, "html/canvas_resize_policy", PROPERTY_HINT_ENUM, "None,Project,Adaptive"), 2));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/focus_canvas_on_start"), true));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/experimental_virtual_keyboard"), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/enabled"), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/offline_page", PROPERTY_HINT_FILE, "*.html"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/display", PROPERTY_HINT_ENUM, "Fullscreen,Standalone,Minimal UI,Browser"), 1));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/orientation", PROPERTY_HINT_ENUM, "Any,Landscape,Portrait"), 0));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_144x144", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg,*.svgz"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_180x180", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg,*.svgz"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/icon_512x512", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg,*.svgz"), ""));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "progressive_web_app/background_color", PROPERTY_HINT_COLOR_NO_ALPHA), Color()));
+}
+
+String EditorExportPlatformJavaScript::get_name() const {
+ return "HTML5";
+}
+
+String EditorExportPlatformJavaScript::get_os_name() const {
+ return "HTML5";
+}
+
+Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const {
+ return logo;
+}
+
+bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const {
+ String err;
+ bool valid = false;
+ ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
+
+ // Look for export templates (first official, and if defined custom templates).
+ bool dvalid = exists_export_template(_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"));
+ if (!dvalid) {
+ err += TTR("Custom debug template not found.") + "\n";
+ }
+ }
+ if (p_preset->get("custom_template/release") != "") {
+ rvalid = FileAccess::exists(p_preset->get("custom_template/release"));
+ if (!rvalid) {
+ err += TTR("Custom release template not found.") + "\n";
+ }
+ }
+
+ valid = dvalid || rvalid;
+ r_missing_templates = !valid;
+
+ // Validate the rest of the configuration.
+
+ if (p_preset->get("vram_texture_compression/for_mobile")) {
+ String etc_error = test_etc2();
+ if (etc_error != String()) {
+ valid = false;
+ err += etc_error;
+ }
+ }
+
+ if (!err.is_empty()) {
+ r_error = err;
+ }
+
+ return valid;
+}
+
+List<String> EditorExportPlatformJavaScript::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const {
+ List<String> list;
+ list.push_back("html");
+ return list;
+}
+
+Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
+ ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
+
+ const String custom_debug = p_preset->get("custom_template/debug");
+ const String custom_release = p_preset->get("custom_template/release");
+ const String custom_html = p_preset->get("html/custom_html_shell");
+ const bool export_icon = p_preset->get("html/export_icon");
+ const bool pwa = p_preset->get("progressive_web_app/enabled");
+
+ const String base_dir = p_path.get_base_dir();
+ const String base_path = p_path.get_basename();
+ const String base_name = p_path.get_file().get_basename();
+
+ // Find the correct template
+ String template_path = p_debug ? custom_debug : custom_release;
+ template_path = template_path.strip_edges();
+ if (template_path == String()) {
+ ExportMode mode = (ExportMode)(int)p_preset->get("variant/export_type");
+ template_path = find_export_template(_get_template_name(mode, p_debug));
+ }
+
+ if (!DirAccess::exists(base_dir)) {
+ return ERR_FILE_BAD_PATH;
+ }
+
+ if (template_path != String() && !FileAccess::exists(template_path)) {
+ EditorNode::get_singleton()->show_warning(TTR("Template file not found:") + "\n" + template_path);
+ return ERR_FILE_NOT_FOUND;
+ }
+
+ // Export pck and shared objects
+ Vector<SharedObject> shared_objects;
+ String pck_path = base_path + ".pck";
+ 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 = 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);
+ da = nullptr;
+
+ // Extract templates.
+ error = _extract_template(template_path, base_dir, base_name, pwa);
+ if (error) {
+ return error;
+ }
+
+ // Parse generated file sizes (pck and wasm, to help show a meaningful loading bar).
+ Dictionary file_sizes;
+ FileAccess *f = nullptr;
+ f = FileAccess::open(pck_path, FileAccess::READ);
+ if (f) {
+ file_sizes[pck_path.get_file()] = (uint64_t)f->get_length();
+ memdelete(f);
+ f = nullptr;
+ }
+ f = FileAccess::open(base_path + ".wasm", FileAccess::READ);
+ if (f) {
+ file_sizes[base_name + ".wasm"] = (uint64_t)f->get_length();
+ memdelete(f);
+ f = nullptr;
+ }
+
+ // Read the HTML shell file (custom or from template).
+ const String html_path = custom_html.is_empty() ? base_path + ".html" : custom_html;
+ Vector<uint8_t> html;
+ f = FileAccess::open(html_path, FileAccess::READ);
+ if (!f) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not read HTML shell:") + "\n" + html_path);
+ return ERR_FILE_CANT_READ;
+ }
+ html.resize(f->get_length());
+ f->get_buffer(html.ptrw(), html.size());
+ memdelete(f);
+ f = nullptr;
+
+ // Generate HTML file with replaced strings.
+ _fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes);
+ Error err = _write_or_error(html.ptr(), html.size(), p_path);
+ if (err != OK) {
+ return err;
+ }
+ html.resize(0);
+
+ // Export splash (why?)
+ Ref<Image> splash = _get_project_splash();
+ const String splash_png_path = base_path + ".png";
+ if (splash->save_png(splash_png_path) != OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + splash_png_path);
+ return ERR_FILE_CANT_WRITE;
+ }
+
+ // Save a favicon that can be accessed without waiting for the project to finish loading.
+ // This way, the favicon can be displayed immediately when loading the page.
+ if (export_icon) {
+ Ref<Image> favicon = _get_project_icon();
+ const String favicon_png_path = base_path + ".icon.png";
+ if (favicon->save_png(favicon_png_path) != OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + favicon_png_path);
+ return ERR_FILE_CANT_WRITE;
+ }
+ favicon->resize(180, 180);
+ const String apple_icon_png_path = base_path + ".apple-touch-icon.png";
+ if (favicon->save_png(apple_icon_png_path) != OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not write file:") + "\n" + apple_icon_png_path);
+ return ERR_FILE_CANT_WRITE;
+ }
+ }
+
+ // Generate the PWA worker and manifest
+ if (pwa) {
+ err = _build_pwa(p_preset, p_path, shared_objects);
+ if (err != OK) {
+ return err;
+ }
+ }
+
+ return OK;
+}
+
+bool EditorExportPlatformJavaScript::poll_export() {
+ Ref<EditorExportPreset> preset;
+
+ for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) {
+ Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i);
+ if (ep->is_runnable() && ep->get_platform() == this) {
+ preset = ep;
+ break;
+ }
+ }
+
+ int prev = menu_options;
+ menu_options = preset.is_valid();
+ if (server->is_listening()) {
+ if (menu_options == 0) {
+ MutexLock lock(server_lock);
+ server->stop();
+ } else {
+ menu_options += 1;
+ }
+ }
+ return menu_options != prev;
+}
+
+Ref<ImageTexture> EditorExportPlatformJavaScript::get_option_icon(int p_index) const {
+ return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
+}
+
+int EditorExportPlatformJavaScript::get_options_count() const {
+ return menu_options;
+}
+
+Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) {
+ if (p_option == 1) {
+ MutexLock lock(server_lock);
+ server->stop();
+ return OK;
+ }
+
+ const String dest = EditorPaths::get_singleton()->get_cache_dir().plus_file("web");
+ DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ if (!da->dir_exists(dest)) {
+ Error err = da->make_dir_recursive(dest);
+ if (err != OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Could not create HTTP server directory:") + "\n" + dest);
+ return err;
+ }
+ }
+ const String basepath = dest.plus_file("tmp_js_export");
+ Error err = export_project(p_preset, true, basepath + ".html", p_debug_flags);
+ if (err != OK) {
+ // Export generates several files, clean them up on failure.
+ DirAccess::remove_file_or_error(basepath + ".html");
+ DirAccess::remove_file_or_error(basepath + ".offline.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 + ".service.worker.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(basepath + ".icon.png");
+ DirAccess::remove_file_or_error(basepath + ".apple-touch-icon.png");
+ return err;
+ }
+
+ const uint16_t bind_port = EDITOR_GET("export/web/http_port");
+ // Resolve host if needed.
+ const String bind_host = EDITOR_GET("export/web/http_host");
+ IPAddress bind_ip;
+ if (bind_host.is_valid_ip_address()) {
+ bind_ip = bind_host;
+ } else {
+ bind_ip = IP::get_singleton()->resolve_hostname(bind_host);
+ }
+ ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + bind_host + "'. Try using '127.0.0.1'.");
+
+ const bool use_ssl = EDITOR_GET("export/web/use_ssl");
+ const String ssl_key = EDITOR_GET("export/web/ssl_key");
+ const String ssl_cert = EDITOR_GET("export/web/ssl_certificate");
+
+ // Restart server.
+ {
+ MutexLock lock(server_lock);
+
+ server->stop();
+ err = server->listen(bind_port, bind_ip, use_ssl, ssl_key, ssl_cert);
+ }
+ if (err != OK) {
+ EditorNode::get_singleton()->show_warning(TTR("Error starting HTTP server:") + "\n" + itos(err));
+ return err;
+ }
+
+ OS::get_singleton()->shell_open(String((use_ssl ? "https://" : "http://") + bind_host + ":" + itos(bind_port) + "/tmp_js_export.html"));
+ // FIXME: Find out how to clean up export files after running the successfully
+ // exported game. Might not be trivial.
+ return OK;
+}
+
+Ref<Texture2D> EditorExportPlatformJavaScript::get_run_icon() const {
+ return run_icon;
+}
+
+void EditorExportPlatformJavaScript::_server_thread_poll(void *data) {
+ EditorExportPlatformJavaScript *ej = (EditorExportPlatformJavaScript *)data;
+ while (!ej->server_quit) {
+ OS::get_singleton()->delay_usec(1000);
+ {
+ MutexLock lock(ej->server_lock);
+ ej->server->poll();
+ }
+ }
+}
+
+EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() {
+ server.instantiate();
+ server_thread.start(_server_thread_poll, this);
+
+ Ref<Image> img = memnew(Image(_javascript_logo));
+ logo.instantiate();
+ logo->create_from_image(img);
+
+ img = Ref<Image>(memnew(Image(_javascript_run_icon)));
+ run_icon.instantiate();
+ run_icon->create_from_image(img);
+
+ Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
+ if (theme.is_valid()) {
+ stop_icon = theme->get_icon("Stop", "EditorIcons");
+ } else {
+ stop_icon.instantiate();
+ }
+}
+
+EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() {
+ server->stop();
+ server_quit = true;
+ server_thread.wait_to_finish();
+}
diff --git a/platform/javascript/export/export_plugin.h b/platform/javascript/export/export_plugin.h
new file mode 100644
index 0000000000..736edfe3a8
--- /dev/null
+++ b/platform/javascript/export/export_plugin.h
@@ -0,0 +1,149 @@
+/*************************************************************************/
+/* export_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_EXPORT_PLUGIN_H
+#define JAVASCRIPT_EXPORT_PLUGIN_H
+
+#include "core/io/image_loader.h"
+#include "core/io/stream_peer_ssl.h"
+#include "core/io/tcp_server.h"
+#include "core/io/zip_io.h"
+#include "editor/editor_export.h"
+#include "editor/editor_node.h"
+#include "main/splash.gen.h"
+#include "platform/javascript/logo.gen.h"
+#include "platform/javascript/run_icon.gen.h"
+
+#include "export_server.h"
+
+class EditorExportPlatformJavaScript : public EditorExportPlatform {
+ GDCLASS(EditorExportPlatformJavaScript, EditorExportPlatform);
+
+ Ref<ImageTexture> logo;
+ Ref<ImageTexture> run_icon;
+ Ref<ImageTexture> stop_icon;
+ int menu_options = 0;
+
+ Ref<EditorHTTPServer> server;
+ bool server_quit = false;
+ Mutex server_lock;
+ 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;
+ }
+
+ Ref<Image> _get_project_icon() const {
+ Ref<Image> icon;
+ icon.instantiate();
+ const String icon_path = String(GLOBAL_GET("application/config/icon")).strip_edges();
+ if (icon_path.is_empty() || ImageLoader::load_image(icon_path, icon) != OK) {
+ return EditorNode::get_singleton()->get_editor_theme()->get_icon("DefaultProjectIcon", "EditorIcons")->get_image();
+ }
+ return icon;
+ }
+
+ Ref<Image> _get_project_splash() const {
+ Ref<Image> splash;
+ splash.instantiate();
+ const String splash_path = String(GLOBAL_GET("application/boot_splash/image")).strip_edges();
+ if (splash_path.is_empty() || ImageLoader::load_image(splash_path, splash) != OK) {
+ return Ref<Image>(memnew(Image(boot_splash_png)));
+ }
+ return splash;
+ }
+
+ Error _extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa);
+ void _replace_strings(Map<String, String> p_replaces, Vector<uint8_t> &r_template);
+ 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, const Dictionary &p_file_sizes);
+ Error _add_manifest_icon(const String &p_path, const String &p_icon, int p_size, Array &r_arr);
+ Error _build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects);
+ Error _write_or_error(const uint8_t *p_content, int p_len, String p_path);
+
+ static void _server_thread_poll(void *data);
+
+public:
+ virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) override;
+
+ virtual void get_export_options(List<ExportOption> *r_options) override;
+
+ virtual String get_name() const override;
+ virtual String get_os_name() const override;
+ virtual Ref<Texture2D> get_logo() const override;
+
+ virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override;
+ virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override;
+ virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;
+
+ virtual bool poll_export() override;
+ virtual int get_options_count() const override;
+ virtual String get_option_label(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); }
+ virtual String get_option_tooltip(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); }
+ virtual Ref<ImageTexture> get_option_icon(int p_index) const override;
+ virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) override;
+ virtual Ref<Texture2D> get_run_icon() const override;
+
+ virtual void get_platform_features(List<String> *r_features) override {
+ r_features->push_back("web");
+ r_features->push_back(get_os_name());
+ }
+
+ virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, Set<String> &p_features) override {
+ }
+
+ String get_debug_protocol() const override { return "ws://"; }
+
+ EditorExportPlatformJavaScript();
+ ~EditorExportPlatformJavaScript();
+};
+
+#endif
diff --git a/platform/javascript/export/export_server.h b/platform/javascript/export/export_server.h
new file mode 100644
index 0000000000..87cbdcab47
--- /dev/null
+++ b/platform/javascript/export/export_server.h
@@ -0,0 +1,254 @@
+/*************************************************************************/
+/* export_server.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_EXPORT_SERVER_H
+#define JAVASCRIPT_EXPORT_SERVER_H
+
+#include "core/io/image_loader.h"
+#include "core/io/stream_peer_ssl.h"
+#include "core/io/tcp_server.h"
+#include "core/io/zip_io.h"
+#include "editor/editor_export.h"
+#include "editor/editor_node.h"
+
+class EditorHTTPServer : public RefCounted {
+private:
+ Ref<TCPServer> server;
+ Map<String, String> mimes;
+ Ref<StreamPeerTCP> tcp;
+ Ref<StreamPeerSSL> ssl;
+ Ref<StreamPeer> peer;
+ Ref<CryptoKey> key;
+ Ref<X509Certificate> cert;
+ bool use_ssl = false;
+ uint64_t time = 0;
+ uint8_t req_buf[4096];
+ int req_pos = 0;
+
+ void _clear_client() {
+ peer = Ref<StreamPeer>();
+ ssl = Ref<StreamPeerSSL>();
+ tcp = Ref<StreamPeerTCP>();
+ memset(req_buf, 0, sizeof(req_buf));
+ time = 0;
+ req_pos = 0;
+ }
+
+ void _set_internal_certs(Ref<Crypto> p_crypto) {
+ const String cache_path = EditorPaths::get_singleton()->get_cache_dir();
+ const String key_path = cache_path.plus_file("html5_server.key");
+ const String crt_path = cache_path.plus_file("html5_server.crt");
+ bool regen = !FileAccess::exists(key_path) || !FileAccess::exists(crt_path);
+ if (!regen) {
+ key = Ref<CryptoKey>(CryptoKey::create());
+ cert = Ref<X509Certificate>(X509Certificate::create());
+ if (key->load(key_path) != OK || cert->load(crt_path) != OK) {
+ regen = true;
+ }
+ }
+ if (regen) {
+ key = p_crypto->generate_rsa(2048);
+ key->save(key_path);
+ cert = p_crypto->generate_self_signed_certificate(key, "CN=godot-debug.local,O=A Game Dev,C=XXA", "20140101000000", "20340101000000");
+ cert->save(crt_path);
+ }
+ }
+
+public:
+ EditorHTTPServer() {
+ mimes["html"] = "text/html";
+ mimes["js"] = "application/javascript";
+ mimes["json"] = "application/json";
+ mimes["pck"] = "application/octet-stream";
+ mimes["png"] = "image/png";
+ mimes["svg"] = "image/svg";
+ mimes["wasm"] = "application/wasm";
+ server.instantiate();
+ stop();
+ }
+
+ void stop() {
+ server->stop();
+ _clear_client();
+ }
+
+ Error listen(int p_port, IPAddress p_address, bool p_use_ssl, String p_ssl_key, String p_ssl_cert) {
+ use_ssl = p_use_ssl;
+ if (use_ssl) {
+ Ref<Crypto> crypto = Crypto::create();
+ if (crypto.is_null()) {
+ return ERR_UNAVAILABLE;
+ }
+ if (!p_ssl_key.is_empty() && !p_ssl_cert.is_empty()) {
+ key = Ref<CryptoKey>(CryptoKey::create());
+ Error err = key->load(p_ssl_key);
+ ERR_FAIL_COND_V(err != OK, err);
+ cert = Ref<X509Certificate>(X509Certificate::create());
+ err = cert->load(p_ssl_cert);
+ ERR_FAIL_COND_V(err != OK, err);
+ } else {
+ _set_internal_certs(crypto);
+ }
+ }
+ return server->listen(p_port, p_address);
+ }
+
+ bool is_listening() const {
+ return server->is_listening();
+ }
+
+ void _send_response() {
+ Vector<String> psa = String((char *)req_buf).split("\r\n");
+ int len = psa.size();
+ ERR_FAIL_COND_MSG(len < 4, "Not enough response headers, got: " + itos(len) + ", expected >= 4.");
+
+ Vector<String> req = psa[0].split(" ", false);
+ ERR_FAIL_COND_MSG(req.size() < 2, "Invalid protocol or status code.");
+
+ // Wrong protocol
+ ERR_FAIL_COND_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", "Invalid method or HTTP version.");
+
+ const int query_index = req[1].find_char('?');
+ const String path = (query_index == -1) ? req[1] : req[1].substr(0, query_index);
+
+ const String req_file = path.get_file();
+ const String req_ext = path.get_extension();
+ const String cache_path = EditorPaths::get_singleton()->get_cache_dir().plus_file("web");
+ const String filepath = cache_path.plus_file(req_file);
+
+ if (!mimes.has(req_ext) || !FileAccess::exists(filepath)) {
+ String s = "HTTP/1.1 404 Not Found\r\n";
+ s += "Connection: Close\r\n";
+ s += "\r\n";
+ CharString cs = s.utf8();
+ peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
+ return;
+ }
+ const String ctype = mimes[req_ext];
+
+ FileAccess *f = FileAccess::open(filepath, FileAccess::READ);
+ ERR_FAIL_COND(!f);
+ String s = "HTTP/1.1 200 OK\r\n";
+ s += "Connection: Close\r\n";
+ s += "Content-Type: " + ctype + "\r\n";
+ s += "Access-Control-Allow-Origin: *\r\n";
+ s += "Cross-Origin-Opener-Policy: same-origin\r\n";
+ s += "Cross-Origin-Embedder-Policy: require-corp\r\n";
+ s += "Cache-Control: no-store, max-age=0\r\n";
+ s += "\r\n";
+ CharString cs = s.utf8();
+ Error err = peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
+ if (err != OK) {
+ memdelete(f);
+ ERR_FAIL();
+ }
+
+ while (true) {
+ uint8_t bytes[4096];
+ uint64_t read = f->get_buffer(bytes, 4096);
+ if (read == 0) {
+ break;
+ }
+ err = peer->put_data(bytes, read);
+ if (err != OK) {
+ memdelete(f);
+ ERR_FAIL();
+ }
+ }
+ memdelete(f);
+ }
+
+ void poll() {
+ if (!server->is_listening()) {
+ return;
+ }
+ if (tcp.is_null()) {
+ if (!server->is_connection_available()) {
+ return;
+ }
+ tcp = server->take_connection();
+ peer = tcp;
+ time = OS::get_singleton()->get_ticks_usec();
+ }
+ if (OS::get_singleton()->get_ticks_usec() - time > 1000000) {
+ _clear_client();
+ return;
+ }
+ if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) {
+ return;
+ }
+
+ if (use_ssl) {
+ if (ssl.is_null()) {
+ ssl = Ref<StreamPeerSSL>(StreamPeerSSL::create());
+ peer = ssl;
+ ssl->set_blocking_handshake_enabled(false);
+ if (ssl->accept_stream(tcp, key, cert) != OK) {
+ _clear_client();
+ return;
+ }
+ }
+ ssl->poll();
+ if (ssl->get_status() == StreamPeerSSL::STATUS_HANDSHAKING) {
+ // Still handshaking, keep waiting.
+ return;
+ }
+ if (ssl->get_status() != StreamPeerSSL::STATUS_CONNECTED) {
+ _clear_client();
+ return;
+ }
+ }
+
+ while (true) {
+ char *r = (char *)req_buf;
+ int l = req_pos - 1;
+ if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') {
+ _send_response();
+ _clear_client();
+ return;
+ }
+
+ int read = 0;
+ ERR_FAIL_COND(req_pos >= 4096);
+ Error err = peer->get_partial_data(&req_buf[req_pos], 1, read);
+ if (err != OK) {
+ // Got an error
+ _clear_client();
+ return;
+ } else if (read != 1) {
+ // Busy, wait next poll
+ return;
+ }
+ req_pos += read;
+ }
+ }
+};
+
+#endif
diff --git a/platform/javascript/http_client.h.inc b/platform/javascript/http_client.h.inc
deleted file mode 100644
index 6544d41c98..0000000000
--- a/platform/javascript/http_client.h.inc
+++ /dev/null
@@ -1,51 +0,0 @@
-/*************************************************************************/
-/* http_client.h.inc */
-/*************************************************************************/
-/* 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. */
-/*************************************************************************/
-
-// HTTPClient's additional private members in the javascript platform
-
-Error make_request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_len);
-static void _parse_headers(int p_len, const char **p_headers, void *p_ref);
-
-int js_id = 0;
-// 64 KiB by default (favors fast download speeds at the cost of memory usage).
-int read_limit = 65536;
-Status status = STATUS_DISCONNECTED;
-
-String host;
-int port = -1;
-bool use_tls = false;
-
-int polled_response_code = 0;
-Vector<String> response_headers;
-Vector<uint8_t> response_buffer;
-
-#ifdef DEBUG_ENABLED
-uint64_t last_polling_frame = 0;
-#endif
diff --git a/platform/javascript/http_client_javascript.cpp b/platform/javascript/http_client_javascript.cpp
index a6cf4b0eb8..f7d78abcea 100644
--- a/platform/javascript/http_client_javascript.cpp
+++ b/platform/javascript/http_client_javascript.cpp
@@ -28,45 +28,19 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-#include "core/io/http_client.h"
+#include "http_client_javascript.h"
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#include "stddef.h"
-
-typedef enum {
- GODOT_JS_FETCH_STATE_REQUESTING = 0,
- GODOT_JS_FETCH_STATE_BODY = 1,
- GODOT_JS_FETCH_STATE_DONE = 2,
- GODOT_JS_FETCH_STATE_ERROR = -1,
-} godot_js_fetch_state_t;
-
-extern int godot_js_fetch_create(const char *p_method, const char *p_url, const char **p_headers, int p_headers_len, const uint8_t *p_body, int p_body_len);
-extern int godot_js_fetch_read_headers(int p_id, void (*parse_callback)(int p_size, const char **p_headers, void *p_ref), void *p_ref);
-extern int godot_js_fetch_read_chunk(int p_id, uint8_t *p_buf, int p_buf_size);
-extern void godot_js_fetch_free(int p_id);
-extern godot_js_fetch_state_t godot_js_fetch_state_get(int p_id);
-extern int godot_js_fetch_body_length_get(int p_id);
-extern int godot_js_fetch_http_status_get(int p_id);
-extern int godot_js_fetch_is_chunked(int p_id);
-
-#ifdef __cplusplus
-}
-#endif
-
-void HTTPClient::_parse_headers(int p_len, const char **p_headers, void *p_ref) {
- HTTPClient *client = static_cast<HTTPClient *>(p_ref);
+void HTTPClientJavaScript::_parse_headers(int p_len, const char **p_headers, void *p_ref) {
+ HTTPClientJavaScript *client = static_cast<HTTPClientJavaScript *>(p_ref);
for (int i = 0; i < p_len; i++) {
client->response_headers.push_back(String::utf8(p_headers[i]));
}
}
-Error HTTPClient::connect_to_host(const String &p_host, int p_port, bool p_ssl, bool p_verify_host) {
+Error HTTPClientJavaScript::connect_to_host(const String &p_host, int p_port, bool p_ssl, bool p_verify_host) {
close();
if (p_ssl && !p_verify_host) {
- WARN_PRINT("Disabling HTTPClient's host verification is not supported for the HTML5 platform, host will be verified");
+ WARN_PRINT("Disabling HTTPClientJavaScript's host verification is not supported for the HTML5 platform, host will be verified");
}
port = p_port;
@@ -97,15 +71,15 @@ Error HTTPClient::connect_to_host(const String &p_host, int p_port, bool p_ssl,
return OK;
}
-void HTTPClient::set_connection(const Ref<StreamPeer> &p_connection) {
- ERR_FAIL_MSG("Accessing an HTTPClient's StreamPeer is not supported for the HTML5 platform.");
+void HTTPClientJavaScript::set_connection(const Ref<StreamPeer> &p_connection) {
+ ERR_FAIL_MSG("Accessing an HTTPClientJavaScript's StreamPeer is not supported for the HTML5 platform.");
}
-Ref<StreamPeer> HTTPClient::get_connection() const {
- ERR_FAIL_V_MSG(REF(), "Accessing an HTTPClient's StreamPeer is not supported for the HTML5 platform.");
+Ref<StreamPeer> HTTPClientJavaScript::get_connection() const {
+ ERR_FAIL_V_MSG(REF(), "Accessing an HTTPClientJavaScript's StreamPeer is not supported for the HTML5 platform.");
}
-Error HTTPClient::make_request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_len) {
+Error HTTPClientJavaScript::request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_len) {
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);
@@ -128,22 +102,7 @@ Error HTTPClient::make_request(Method p_method, const String &p_url, const Vecto
return OK;
}
-Error HTTPClient::request_raw(Method p_method, const String &p_url, const Vector<String> &p_headers, const Vector<uint8_t> &p_body) {
- if (p_body.is_empty()) {
- return make_request(p_method, p_url, p_headers, nullptr, 0);
- }
- return make_request(p_method, p_url, p_headers, p_body.ptr(), p_body.size());
-}
-
-Error HTTPClient::request(Method p_method, const String &p_url, const Vector<String> &p_headers, const String &p_body) {
- if (p_body.is_empty()) {
- return make_request(p_method, p_url, p_headers, nullptr, 0);
- }
- const CharString cs = p_body.utf8();
- return make_request(p_method, p_url, p_headers, (const uint8_t *)cs.get_data(), cs.size() - 1);
-}
-
-void HTTPClient::close() {
+void HTTPClientJavaScript::close() {
host = "";
port = -1;
use_tls = false;
@@ -157,23 +116,23 @@ void HTTPClient::close() {
}
}
-HTTPClient::Status HTTPClient::get_status() const {
+HTTPClientJavaScript::Status HTTPClientJavaScript::get_status() const {
return status;
}
-bool HTTPClient::has_response() const {
+bool HTTPClientJavaScript::has_response() const {
return response_headers.size() > 0;
}
-bool HTTPClient::is_response_chunked() const {
+bool HTTPClientJavaScript::is_response_chunked() const {
return godot_js_fetch_is_chunked(js_id);
}
-int HTTPClient::get_response_code() const {
+int HTTPClientJavaScript::get_response_code() const {
return polled_response_code;
}
-Error HTTPClient::get_response_headers(List<String> *r_response) {
+Error HTTPClientJavaScript::get_response_headers(List<String> *r_response) {
if (!response_headers.size()) {
return ERR_INVALID_PARAMETER;
}
@@ -184,11 +143,11 @@ Error HTTPClient::get_response_headers(List<String> *r_response) {
return OK;
}
-int HTTPClient::get_response_body_length() const {
+int HTTPClientJavaScript::get_response_body_length() const {
return godot_js_fetch_body_length_get(js_id);
}
-PackedByteArray HTTPClient::read_response_body_chunk() {
+PackedByteArray HTTPClientJavaScript::read_response_body_chunk() {
ERR_FAIL_COND_V(status != STATUS_BODY, PackedByteArray());
if (response_buffer.size() != read_limit) {
@@ -213,23 +172,23 @@ PackedByteArray HTTPClient::read_response_body_chunk() {
return chunk;
}
-void HTTPClient::set_blocking_mode(bool p_enable) {
- ERR_FAIL_COND_MSG(p_enable, "HTTPClient blocking mode is not supported for the HTML5 platform.");
+void HTTPClientJavaScript::set_blocking_mode(bool p_enable) {
+ ERR_FAIL_COND_MSG(p_enable, "HTTPClientJavaScript blocking mode is not supported for the HTML5 platform.");
}
-bool HTTPClient::is_blocking_mode_enabled() const {
+bool HTTPClientJavaScript::is_blocking_mode_enabled() const {
return false;
}
-void HTTPClient::set_read_chunk_size(int p_size) {
+void HTTPClientJavaScript::set_read_chunk_size(int p_size) {
read_limit = p_size;
}
-int HTTPClient::get_read_chunk_size() const {
+int HTTPClientJavaScript::get_read_chunk_size() const {
return read_limit;
}
-Error HTTPClient::poll() {
+Error HTTPClientJavaScript::poll() {
switch (status) {
case STATUS_DISCONNECTED:
return ERR_UNCONFIGURED;
@@ -263,7 +222,7 @@ Error HTTPClient::poll() {
#ifdef DEBUG_ENABLED
// forcing synchronous requests is not possible on the web
if (last_polling_frame == Engine::get_singleton()->get_process_frames()) {
- WARN_PRINT("HTTPClient polled multiple times in one frame, "
+ WARN_PRINT("HTTPClientJavaScript polled multiple times in one frame, "
"but request cannot progress more than once per "
"frame on the HTML5 platform.");
}
@@ -294,9 +253,15 @@ Error HTTPClient::poll() {
return OK;
}
-HTTPClient::HTTPClient() {
+HTTPClient *HTTPClientJavaScript::_create_func() {
+ return memnew(HTTPClientJavaScript);
+}
+
+HTTPClient *(*HTTPClient::_create)() = HTTPClientJavaScript::_create_func;
+
+HTTPClientJavaScript::HTTPClientJavaScript() {
}
-HTTPClient::~HTTPClient() {
+HTTPClientJavaScript::~HTTPClientJavaScript() {
close();
}
diff --git a/platform/javascript/http_client_javascript.h b/platform/javascript/http_client_javascript.h
new file mode 100644
index 0000000000..33f91f67b6
--- /dev/null
+++ b/platform/javascript/http_client_javascript.h
@@ -0,0 +1,108 @@
+/*************************************************************************/
+/* http_client_javascript.h */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+
+#ifndef HTTP_CLIENT_JAVASCRIPT_H
+#define HTTP_CLIENT_JAVASCRIPT_H
+
+#include "core/io/http_client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "stddef.h"
+
+typedef enum {
+ GODOT_JS_FETCH_STATE_REQUESTING = 0,
+ GODOT_JS_FETCH_STATE_BODY = 1,
+ GODOT_JS_FETCH_STATE_DONE = 2,
+ GODOT_JS_FETCH_STATE_ERROR = -1,
+} godot_js_fetch_state_t;
+
+extern int godot_js_fetch_create(const char *p_method, const char *p_url, const char **p_headers, int p_headers_len, const uint8_t *p_body, int p_body_len);
+extern int godot_js_fetch_read_headers(int p_id, void (*parse_callback)(int p_size, const char **p_headers, void *p_ref), void *p_ref);
+extern int godot_js_fetch_read_chunk(int p_id, uint8_t *p_buf, int p_buf_size);
+extern void godot_js_fetch_free(int p_id);
+extern godot_js_fetch_state_t godot_js_fetch_state_get(int p_id);
+extern int godot_js_fetch_body_length_get(int p_id);
+extern int godot_js_fetch_http_status_get(int p_id);
+extern int godot_js_fetch_is_chunked(int p_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+class HTTPClientJavaScript : public HTTPClient {
+private:
+ int js_id = 0;
+ Status status = STATUS_DISCONNECTED;
+
+ // 64 KiB by default (favors fast download speeds at the cost of memory usage).
+ int read_limit = 65536;
+
+ String host;
+ int port = -1;
+ bool use_tls = false;
+
+ int polled_response_code = 0;
+ Vector<String> response_headers;
+ Vector<uint8_t> response_buffer;
+
+#ifdef DEBUG_ENABLED
+ uint64_t last_polling_frame = 0;
+#endif
+
+ static void _parse_headers(int p_len, const char **p_headers, void *p_ref);
+
+public:
+ static HTTPClient *_create_func();
+
+ Error request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_size) override;
+
+ Error connect_to_host(const String &p_host, int p_port = -1, bool p_ssl = false, bool p_verify_host = true) override;
+ void set_connection(const Ref<StreamPeer> &p_connection) override;
+ Ref<StreamPeer> get_connection() const override;
+ void close() override;
+ Status get_status() const override;
+ bool has_response() const override;
+ bool is_response_chunked() const override;
+ int get_response_code() const override;
+ Error get_response_headers(List<String> *r_response) override;
+ int get_response_body_length() const override;
+ PackedByteArray read_response_body_chunk() override;
+ void set_blocking_mode(bool p_enable) override;
+ bool is_blocking_mode_enabled() const override;
+ void set_read_chunk_size(int p_size) override;
+ int get_read_chunk_size() const override;
+ Error poll() override;
+ HTTPClientJavaScript();
+ ~HTTPClientJavaScript();
+};
+#endif // HTTP_CLIENT_JAVASCRIPT_H
diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp
index 40771d1882..a3f0dbaa45 100644
--- a/platform/javascript/javascript_main.cpp
+++ b/platform/javascript/javascript_main.cpp
@@ -97,7 +97,7 @@ extern EMSCRIPTEN_KEEPALIVE int godot_js_main(int argc, char *argv[]) {
if (Main::is_project_manager() && FileAccess::exists("/tmp/preload.zip")) {
PackedStringArray ps;
ps.push_back("/tmp/preload.zip");
- os->get_main_loop()->emit_signal("files_dropped", ps, -1);
+ os->get_main_loop()->emit_signal(SNAME("files_dropped"), ps, -1);
}
#endif
emscripten_set_main_loop(main_loop_callback, -1, false);
diff --git a/platform/javascript/javascript_singleton.cpp b/platform/javascript/javascript_singleton.cpp
index c441ed0517..1dd73ef8e9 100644
--- a/platform/javascript/javascript_singleton.cpp
+++ b/platform/javascript/javascript_singleton.cpp
@@ -54,6 +54,7 @@ extern int godot_js_wrapper_object_setvar(int p_id, int p_key_type, godot_js_wra
extern void godot_js_wrapper_object_set(int p_id, const char *p_name, int p_type, godot_js_wrapper_ex *p_val);
extern void godot_js_wrapper_object_unref(int p_id);
extern int godot_js_wrapper_create_cb(void *p_ref, void (*p_callback)(void *p_ref, int p_arg_id, int p_argc));
+extern void godot_js_wrapper_object_set_cb_ret(int p_type, godot_js_wrapper_ex *p_val);
extern int godot_js_wrapper_create_object(const char *p_method, void **p_args, int p_argc, GodotJSWrapperVariant2JSCallback p_variant2js_callback, godot_js_wrapper_ex *p_cb_rval, void **p_lock, GodotJSWrapperFreeLockCallback p_lock_callback);
};
@@ -182,7 +183,7 @@ Variant JavaScriptObjectImpl::_js2variant(int p_type, godot_js_wrapper_ex *p_val
case Variant::FLOAT:
return p_val->r;
case Variant::STRING: {
- String out((const char *)p_val->p);
+ String out = String::utf8((const char *)p_val->p);
free(p_val->p);
return out;
}
@@ -257,6 +258,16 @@ void JavaScriptObjectImpl::_callback(void *p_ref, int p_args_id, int p_argc) {
Callable::CallError err;
Variant ret;
obj->_callable.call(argv, 1, ret, err);
+
+ // Set return value
+ godot_js_wrapper_ex exchange;
+ void *lock = nullptr;
+ const Variant *v = &ret;
+ int type = _variant2js((const void **)&v, 0, &exchange, &lock);
+ godot_js_wrapper_object_set_cb_ret(type, &exchange);
+ if (lock) {
+ _free_lock(&lock, type);
+ }
}
Ref<JavaScriptObject> JavaScript::create_callback(const Callable &p_callable) {
diff --git a/platform/javascript/js/engine/config.js b/platform/javascript/js/engine/config.js
index 6072782875..ba61b14eb7 100644
--- a/platform/javascript/js/engine/config.js
+++ b/platform/javascript/js/engine/config.js
@@ -91,6 +91,14 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
*/
args: [],
/**
+ * When enabled, the game canvas will automatically grab the focus when the engine starts.
+ *
+ * @memberof EngineConfig
+ * @type {boolean}
+ * @default
+ */
+ focusCanvas: true,
+ /**
* When enabled, this will turn on experimental virtual keyboard support on mobile.
*
* @memberof EngineConfig
@@ -238,6 +246,7 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
this.persistentPaths = parse('persistentPaths', this.persistentPaths);
this.persistentDrops = parse('persistentDrops', this.persistentDrops);
this.experimentalVK = parse('experimentalVK', this.experimentalVK);
+ this.focusCanvas = parse('focusCanvas', this.focusCanvas);
this.gdnativeLibs = parse('gdnativeLibs', this.gdnativeLibs);
this.fileSizes = parse('fileSizes', this.fileSizes);
this.args = parse('args', this.args);
@@ -324,6 +333,7 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
'locale': locale,
'persistentDrops': this.persistentDrops,
'virtualKeyboard': this.experimentalVK,
+ 'focusCanvas': this.focusCanvas,
'onExecute': this.onExecute,
'onExit': function (p_code) {
cleanup(); // We always need to call the cleanup callback to free memory.
diff --git a/platform/javascript/js/libs/audio.worklet.js b/platform/javascript/js/libs/audio.worklet.js
index 6b3f80c6a9..866f845139 100644
--- a/platform/javascript/js/libs/audio.worklet.js
+++ b/platform/javascript/js/libs/audio.worklet.js
@@ -115,7 +115,7 @@ class GodotProcessor extends AudioWorkletProcessor {
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.running = false;
this.output = null;
this.input = null;
}
diff --git a/platform/javascript/js/libs/library_godot_javascript_singleton.js b/platform/javascript/js/libs/library_godot_javascript_singleton.js
index cb80273ca8..22ce003cd2 100644
--- a/platform/javascript/js/libs/library_godot_javascript_singleton.js
+++ b/platform/javascript/js/libs/library_godot_javascript_singleton.js
@@ -34,6 +34,7 @@ const GodotJSWrapper = {
$GodotJSWrapper__postset: 'GodotJSWrapper.proxies = new Map();',
$GodotJSWrapper: {
proxies: null,
+ cb_ret: null,
MyProxy: function (val) {
const id = IDHandler.add(this);
@@ -202,15 +203,27 @@ const GodotJSWrapper = {
let id = 0;
const cb = function () {
if (!GodotJSWrapper.get_proxied_value(id)) {
- return;
+ return undefined;
}
+ // The callback will store the returned value in this variable via
+ // "godot_js_wrapper_object_set_cb_ret" upon calling the user function.
+ // This is safe! JavaScript is single threaded (and using it in threads is not a good idea anyway).
+ GodotJSWrapper.cb_ret = null;
const args = Array.from(arguments);
func(p_ref, GodotJSWrapper.get_proxied(args), args.length);
+ const ret = GodotJSWrapper.cb_ret;
+ GodotJSWrapper.cb_ret = null;
+ return ret;
};
id = GodotJSWrapper.get_proxied(cb);
return id;
},
+ godot_js_wrapper_object_set_cb_ret__sig: 'vii',
+ godot_js_wrapper_object_set_cb_ret: function (p_val_type, p_val_ex) {
+ GodotJSWrapper.cb_ret = GodotJSWrapper.variant2js(p_val_type, p_val_ex);
+ },
+
godot_js_wrapper_object_getvar__sig: 'iiii',
godot_js_wrapper_object_getvar: function (p_id, p_type, p_exchange) {
const obj = GodotJSWrapper.get_proxied_value(p_id);
diff --git a/platform/javascript/js/libs/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js
index 7414b8cc47..99e7ee8b5f 100644
--- a/platform/javascript/js/libs/library_godot_os.js
+++ b/platform/javascript/js/libs/library_godot_os.js
@@ -72,6 +72,9 @@ const GodotConfig = {
GodotConfig.persistent_drops = !!p_opts['persistentDrops'];
GodotConfig.on_execute = p_opts['onExecute'];
GodotConfig.on_exit = p_opts['onExit'];
+ if (p_opts['focusCanvas']) {
+ GodotConfig.canvas.focus();
+ }
},
locate_file: function (file) {
@@ -103,7 +106,7 @@ autoAddDeps(GodotConfig, '$GodotConfig');
mergeInto(LibraryManager.library, GodotConfig);
const GodotFS = {
- $GodotFS__deps: ['$FS', '$IDBFS', '$GodotRuntime'],
+ $GodotFS__deps: ['$ERRNO_CODES', '$FS', '$IDBFS', '$GodotRuntime'],
$GodotFS__postset: [
'Module["initFS"] = GodotFS.init;',
'Module["copyToFS"] = GodotFS.copy_to_fs;',
diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp
index 0b1650076c..76102d941b 100644
--- a/platform/javascript/os_javascript.cpp
+++ b/platform/javascript/os_javascript.cpp
@@ -31,13 +31,12 @@
#include "os_javascript.h"
#include "core/debugger/engine_debugger.h"
-#include "core/io/json.h"
#include "drivers/unix/dir_access_unix.h"
#include "drivers/unix/file_access_unix.h"
#include "main/main.h"
-#include "modules/modules_enabled.gen.h"
#include "platform/javascript/display_server_javascript.h"
+#include "modules/modules_enabled.gen.h"
#ifdef MODULE_WEBSOCKET_ENABLED
#include "modules/websocket/remote_debugger_peer_websocket.h"
#endif
@@ -48,6 +47,10 @@
#include "godot_js.h"
+void OS_JavaScript::alert(const String &p_alert, const String &p_title) {
+ godot_js_display_alert(p_alert.utf8().get_data());
+}
+
// Lifecycle
void OS_JavaScript::initialize() {
OS_Unix::initialize_core();
@@ -112,10 +115,10 @@ Error OS_JavaScript::execute(const String &p_path, const List<String> &p_argumen
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());
+ for (const String &E : p_arguments) {
+ args.push_back(E);
}
- String json_args = JSON::print(args);
+ String json_args = Variant(args).to_json_string();
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;
diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h
index 81bb9c5f3d..efac2dbca7 100644
--- a/platform/javascript/os_javascript.h
+++ b/platform/javascript/os_javascript.h
@@ -89,6 +89,9 @@ public:
String get_user_data_dir() const override;
bool is_userfs_persistent() const override;
+
+ void alert(const String &p_alert, const String &p_title = "ALERT!") override;
+
Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) override;
void resume_audio();