diff options
Diffstat (limited to 'platform/javascript')
20 files changed, 1536 insertions, 1152 deletions
diff --git a/platform/javascript/.eslintrc.js b/platform/javascript/.eslintrc.js index 0ff9d67d26..2c81f1f02d 100644 --- a/platform/javascript/.eslintrc.js +++ b/platform/javascript/.eslintrc.js @@ -39,5 +39,13 @@ module.exports = { // Closure compiler (exported properties) "quote-props": ["error", "consistent"], "dot-notation": "off", + // No comma dangle for functions (it's madness, and ES2017) + "comma-dangle": ["error", { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "never" + }], } }; diff --git a/platform/javascript/README.md b/platform/javascript/README.md new file mode 100644 index 0000000000..f181bea9e0 --- /dev/null +++ b/platform/javascript/README.md @@ -0,0 +1,15 @@ +# HTML5 platform port + +This folder contains the C++ and JavaScript code for the HTML5/WebAssembly platform port, +compiled using [Emscripten](https://emscripten.org/). + +It also contains a ESLint linting setup (see [`package.json`](package.json)). + +See also [`misc/dist/html`](/misc/dist/html) folder for files used by this platform +such as the HTML5 shell. + +## Artwork license + +[`logo.png`](logo.png) and [`run_icon.png`](run_icon.png) are licensed under +[Creative Commons Attribution 3.0 Unported](https://www.w3.org/html/logo/faq.html#how-licenced) +per the HTML5 logo usage guidelines. diff --git a/platform/javascript/api/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp index 54f541f607..45a2cd595a 100644 --- a/platform/javascript/api/javascript_tools_editor_plugin.cpp +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -35,6 +35,7 @@ #include "core/config/project_settings.h" #include "core/io/dir_access.h" #include "core/io/file_access.h" +#include "core/os/time.h" #include "editor/editor_node.h" #include <emscripten/emscripten.h> @@ -58,23 +59,40 @@ JavaScriptToolsEditorPlugin::JavaScriptToolsEditorPlugin(EditorNode *p_editor) { void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { if (!Engine::get_singleton() || !Engine::get_singleton()->is_editor_hint()) { - WARN_PRINT("Project download is only available in Editor mode"); + ERR_PRINT("Downloading the project as a ZIP archive is only available in Editor mode."); return; } String resource_path = ProjectSettings::get_singleton()->get_resource_path(); FileAccess *src_f; zlib_filefunc_def io = zipio_create_io_from_file(&src_f); - zipFile zip = zipOpen2("/tmp/project.zip", APPEND_STATUS_CREATE, nullptr, &io); - String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; + + // Name the downloded ZIP file to contain the project name and download date for easier organization. + // Replace characters not allowed (or risky) in Windows file names with safe characters. + // In the project name, all invalid characters become an empty string so that a name + // like "Platformer 2: Godette's Revenge" becomes "platformer_2-_godette-s_revenge". + const String project_name = GLOBAL_GET("application/config/name"); + const String project_name_safe = project_name.to_lower().replace(" ", "_"); + const String datetime_safe = + Time::get_singleton()->get_datetime_string_from_system(false, true).replace(" ", "_"); + const String output_name = OS::get_singleton()->get_safe_dir_name(vformat("%s_%s.zip")); + const String output_path = String("/tmp").plus_file(output_name); + + zipFile zip = zipOpen2(output_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io); + const String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; _zip_recursive(resource_path, base_path, zip); zipClose(zip, nullptr); - FileAccess *f = FileAccess::open("/tmp/project.zip", FileAccess::READ); - ERR_FAIL_COND_MSG(!f, "Unable to create zip file"); + FileAccess *f = FileAccess::open(output_path, FileAccess::READ); + ERR_FAIL_COND_MSG(!f, "Unable to create ZIP file."); Vector<uint8_t> buf; buf.resize(f->get_length()); f->get_buffer(buf.ptrw(), buf.size()); - godot_js_os_download_buffer(buf.ptr(), buf.size(), "project.zip", "application/zip"); + godot_js_os_download_buffer(buf.ptr(), buf.size(), output_name.utf8().get_data(), "application/zip"); + + f->close(); + memdelete(f); + // Remove the temporary file since it was sent to the user's native filesystem as a download. + DirAccess::remove_file_or_error(output_path); } void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, zipFile p_zip) { @@ -108,7 +126,7 @@ void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, z void JavaScriptToolsEditorPlugin::_zip_recursive(String p_path, String p_base_path, zipFile p_zip) { DirAccess *dir = DirAccess::open(p_path); if (!dir) { - WARN_PRINT("Unable to open dir for zipping: " + p_path); + WARN_PRINT("Unable to open directory for zipping: " + p_path); return; } dir->list_dir_begin(); diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 478e848675..cfe6c69072 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -34,22 +34,18 @@ #include <emscripten.h> -AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr; +AudioDriverJavaScript::AudioContext AudioDriverJavaScript::audio_context; bool AudioDriverJavaScript::is_available() { return godot_audio_is_available() != 0; } -const char *AudioDriverJavaScript::get_name() const { - return "JavaScript"; -} - void AudioDriverJavaScript::_state_change_callback(int p_state) { - singleton->state = p_state; + AudioDriverJavaScript::audio_context.state = p_state; } void AudioDriverJavaScript::_latency_update_callback(float p_latency) { - singleton->output_latency = p_latency; + AudioDriverJavaScript::audio_context.output_latency = p_latency; } void AudioDriverJavaScript::_audio_driver_process(int p_from, int p_samples) { @@ -105,17 +101,19 @@ void AudioDriverJavaScript::_audio_driver_capture(int p_from, int p_samples) { } Error AudioDriverJavaScript::init() { - mix_rate = GLOBAL_GET("audio/driver/mix_rate"); int latency = GLOBAL_GET("audio/driver/output_latency"); - - channel_count = godot_audio_init(mix_rate, latency, &_state_change_callback, &_latency_update_callback); + if (!audio_context.inited) { + audio_context.mix_rate = GLOBAL_GET("audio/driver/mix_rate"); + audio_context.channel_count = godot_audio_init(&audio_context.mix_rate, latency, &_state_change_callback, &_latency_update_callback); + audio_context.inited = true; + } + mix_rate = audio_context.mix_rate; + channel_count = audio_context.channel_count; buffer_length = closest_power_of_2((latency * mix_rate / 1000)); -#ifndef NO_THREADS - node = memnew(WorkletNode); -#else - node = memnew(ScriptProcessorNode); -#endif - buffer_length = node->create(buffer_length, channel_count); + Error err = create(buffer_length, channel_count); + if (err != OK) { + return err; + } if (output_rb) { memdelete_arr(output_rb); } @@ -134,19 +132,17 @@ Error AudioDriverJavaScript::init() { } void AudioDriverJavaScript::start() { - if (node) { - node->start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb)); - } + start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb)); } void AudioDriverJavaScript::resume() { - if (state == 0) { // 'suspended' + if (audio_context.state == 0) { // 'suspended' godot_audio_resume(); } } float AudioDriverJavaScript::get_latency() { - return output_latency + (float(buffer_length) / mix_rate); + return audio_context.output_latency + (float(buffer_length) / mix_rate); } int AudioDriverJavaScript::get_mix_rate() const { @@ -157,24 +153,8 @@ AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const { return get_speaker_mode_by_total_channels(channel_count); } -void AudioDriverJavaScript::lock() { - if (node) { - node->unlock(); - } -} - -void AudioDriverJavaScript::unlock() { - if (node) { - node->unlock(); - } -} - void AudioDriverJavaScript::finish() { - if (node) { - node->finish(); - memdelete(node); - node = nullptr; - } + finish_driver(); if (output_rb) { memdelete_arr(output_rb); output_rb = nullptr; @@ -203,41 +183,66 @@ Error AudioDriverJavaScript::capture_stop() { return OK; } -AudioDriverJavaScript::AudioDriverJavaScript() { - singleton = this; -} - #ifdef NO_THREADS /// ScriptProcessorNode implementation -void AudioDriverJavaScript::ScriptProcessorNode::_process_callback() { - AudioDriverJavaScript::singleton->_audio_driver_capture(); - AudioDriverJavaScript::singleton->_audio_driver_process(); +AudioDriverScriptProcessor *AudioDriverScriptProcessor::singleton = nullptr; + +void AudioDriverScriptProcessor::_process_callback() { + AudioDriverScriptProcessor::singleton->_audio_driver_capture(); + AudioDriverScriptProcessor::singleton->_audio_driver_process(); } -int AudioDriverJavaScript::ScriptProcessorNode::create(int p_buffer_samples, int p_channels) { - return godot_audio_script_create(p_buffer_samples, p_channels); +Error AudioDriverScriptProcessor::create(int &p_buffer_samples, int p_channels) { + if (!godot_audio_has_script_processor()) { + return ERR_UNAVAILABLE; + } + return (Error)godot_audio_script_create(&p_buffer_samples, p_channels); } -void AudioDriverJavaScript::ScriptProcessorNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { +void AudioDriverScriptProcessor::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { godot_audio_script_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, &_process_callback); } + +/// AudioWorkletNode implementation (no threads) +AudioDriverWorklet *AudioDriverWorklet::singleton = nullptr; + +Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) { + if (!godot_audio_has_worklet()) { + return ERR_UNAVAILABLE; + } + return (Error)godot_audio_worklet_create(p_channels); +} + +void AudioDriverWorklet::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { + _audio_driver_process(); + godot_audio_worklet_start_no_threads(p_out_buf, p_out_buf_size, &_process_callback, p_in_buf, p_in_buf_size, &_capture_callback); +} + +void AudioDriverWorklet::_process_callback(int p_pos, int p_samples) { + AudioDriverWorklet *driver = AudioDriverWorklet::singleton; + driver->_audio_driver_process(p_pos, p_samples); +} + +void AudioDriverWorklet::_capture_callback(int p_pos, int p_samples) { + AudioDriverWorklet *driver = AudioDriverWorklet::singleton; + driver->_audio_driver_capture(p_pos, p_samples); +} #else -/// AudioWorkletNode implementation -void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) { - AudioDriverJavaScript::WorkletNode *obj = static_cast<AudioDriverJavaScript::WorkletNode *>(p_data); - AudioDriverJavaScript *driver = AudioDriverJavaScript::singleton; - const int out_samples = memarr_len(driver->output_rb); - const int in_samples = memarr_len(driver->input_rb); +/// AudioWorkletNode implementation (threads) +void AudioDriverWorklet::_audio_thread_func(void *p_data) { + AudioDriverWorklet *driver = static_cast<AudioDriverWorklet *>(p_data); + const int out_samples = memarr_len(driver->get_output_rb()); + const int in_samples = memarr_len(driver->get_input_rb()); int wpos = 0; int to_write = out_samples; int rpos = 0; int to_read = 0; int32_t step = 0; - while (!obj->quit) { + while (!driver->quit) { if (to_read) { driver->lock(); driver->_audio_driver_capture(rpos, to_read); - godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_IN, -to_read); + godot_audio_worklet_state_add(driver->state, STATE_SAMPLES_IN, -to_read); driver->unlock(); rpos += to_read; if (rpos >= in_samples) { @@ -247,38 +252,40 @@ void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) { if (to_write) { driver->lock(); driver->_audio_driver_process(wpos, to_write); - godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_OUT, to_write); + godot_audio_worklet_state_add(driver->state, STATE_SAMPLES_OUT, to_write); driver->unlock(); wpos += to_write; if (wpos >= out_samples) { wpos -= out_samples; } } - step = godot_audio_worklet_state_wait(obj->state, STATE_PROCESS, step, 1); - to_write = out_samples - godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_OUT); - to_read = godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_IN); + step = godot_audio_worklet_state_wait(driver->state, STATE_PROCESS, step, 1); + to_write = out_samples - godot_audio_worklet_state_get(driver->state, STATE_SAMPLES_OUT); + to_read = godot_audio_worklet_state_get(driver->state, STATE_SAMPLES_IN); } } -int AudioDriverJavaScript::WorkletNode::create(int p_buffer_size, int p_channels) { - godot_audio_worklet_create(p_channels); - return p_buffer_size; +Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) { + if (!godot_audio_has_worklet()) { + return ERR_UNAVAILABLE; + } + return (Error)godot_audio_worklet_create(p_channels); } -void AudioDriverJavaScript::WorkletNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { +void AudioDriverWorklet::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) { godot_audio_worklet_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, state); thread.start(_audio_thread_func, this); } -void AudioDriverJavaScript::WorkletNode::lock() { +void AudioDriverWorklet::lock() { mutex.lock(); } -void AudioDriverJavaScript::WorkletNode::unlock() { +void AudioDriverWorklet::unlock() { mutex.unlock(); } -void AudioDriverJavaScript::WorkletNode::finish() { +void AudioDriverWorklet::finish_driver() { quit = true; // Ask thread to quit. thread.wait_to_finish(); } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index 393693640f..6a0b4bcb0e 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -38,52 +38,15 @@ #include "godot_audio.h" class AudioDriverJavaScript : public AudioDriver { -public: - class AudioNode { - public: - virtual int create(int p_buffer_size, int p_output_channels) = 0; - virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0; - virtual void finish() {} - virtual void lock() {} - virtual void unlock() {} - virtual ~AudioNode() {} - }; - - class WorkletNode : public AudioNode { - private: - enum { - STATE_LOCK, - STATE_PROCESS, - STATE_SAMPLES_IN, - STATE_SAMPLES_OUT, - STATE_MAX, - }; - Mutex mutex; - Thread thread; - bool quit = false; - int32_t state[STATE_MAX] = { 0 }; - - static void _audio_thread_func(void *p_data); - - public: - int create(int p_buffer_size, int p_output_channels) override; - void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; - void finish() override; - void lock() override; - void unlock() override; - }; - - class ScriptProcessorNode : public AudioNode { - private: - static void _process_callback(); - - public: - int create(int p_buffer_samples, int p_channels) override; - void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; - }; - private: - AudioNode *node = nullptr; + struct AudioContext { + bool inited = false; + float output_latency = 0.0; + int state = -1; + int channel_count = 0; + int mix_rate = 0; + }; + static AudioContext audio_context; float *output_rb = nullptr; float *input_rb = nullptr; @@ -91,36 +54,108 @@ private: int buffer_length = 0; int mix_rate = 0; int channel_count = 0; - int state = 0; - float output_latency = 0.0; static void _state_change_callback(int p_state); static void _latency_update_callback(float p_latency); + static AudioDriverJavaScript *singleton; + protected: void _audio_driver_process(int p_from = 0, int p_samples = 0); void _audio_driver_capture(int p_from = 0, int p_samples = 0); + float *get_output_rb() const { return output_rb; } + float *get_input_rb() const { return input_rb; } + + virtual Error create(int &p_buffer_samples, int p_channels) = 0; + virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0; + virtual void finish_driver() {} public: static bool is_available(); - static AudioDriverJavaScript *singleton; + virtual Error init() final; + virtual void start() final; + virtual void finish() final; + + virtual float get_latency() override; + virtual int get_mix_rate() const override; + virtual SpeakerMode get_speaker_mode() const override; + + virtual Error capture_start() override; + virtual Error capture_stop() override; + + static void resume(); + + AudioDriverJavaScript() {} +}; + +#ifdef NO_THREADS +class AudioDriverScriptProcessor : public AudioDriverJavaScript { +private: + static void _process_callback(); + + static AudioDriverScriptProcessor *singleton; + +protected: + Error create(int &p_buffer_samples, int p_channels) override; + void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; + +public: + virtual const char *get_name() const override { return "ScriptProcessor"; } + + virtual void lock() override {} + virtual void unlock() override {} + + AudioDriverScriptProcessor() { singleton = this; } +}; + +class AudioDriverWorklet : public AudioDriverJavaScript { +private: + static void _process_callback(int p_pos, int p_samples); + static void _capture_callback(int p_pos, int p_samples); + + static AudioDriverWorklet *singleton; + +protected: + virtual Error create(int &p_buffer_size, int p_output_channels) override; + virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; - virtual const char *get_name() const; +public: + virtual const char *get_name() const override { return "AudioWorklet"; } + + virtual void lock() override {} + virtual void unlock() override {} - virtual Error init(); - virtual void start(); - void resume(); - virtual float get_latency(); - virtual int get_mix_rate() const; - virtual SpeakerMode get_speaker_mode() const; - virtual void lock(); - virtual void unlock(); - virtual void finish(); + AudioDriverWorklet() { singleton = this; } +}; +#else +class AudioDriverWorklet : public AudioDriverJavaScript { +private: + enum { + STATE_LOCK, + STATE_PROCESS, + STATE_SAMPLES_IN, + STATE_SAMPLES_OUT, + STATE_MAX, + }; + Mutex mutex; + Thread thread; + bool quit = false; + int32_t state[STATE_MAX] = { 0 }; - virtual Error capture_start(); - virtual Error capture_stop(); + static void _audio_thread_func(void *p_data); - AudioDriverJavaScript(); +protected: + virtual Error create(int &p_buffer_size, int p_output_channels) override; + virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override; + virtual void finish_driver() override; + +public: + virtual const char *get_name() const override { return "AudioWorklet"; } + + void lock() override; + void unlock() override; }; #endif + +#endif diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index 8df81bb8cc..124b4ee1c8 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -158,6 +158,10 @@ EM_BOOL DisplayServerJavaScript::keydown_callback(int p_event_type, const Emscri return false; } Input::get_singleton()->parse_input_event(ev); + + // Make sure to flush all events so we can call restricted APIs inside the event. + Input::get_singleton()->flush_buffered_events(); + return true; } @@ -165,6 +169,10 @@ EM_BOOL DisplayServerJavaScript::keypress_callback(int p_event_type, const Emscr DisplayServerJavaScript *display = get_singleton(); display->deferred_key_event->set_unicode(p_event->charCode); Input::get_singleton()->parse_input_event(display->deferred_key_event); + + // Make sure to flush all events so we can call restricted APIs inside the event. + Input::get_singleton()->flush_buffered_events(); + return true; } @@ -172,7 +180,11 @@ 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; + + // Make sure to flush all events so we can call restricted APIs inside the event. + Input::get_singleton()->flush_buffered_events(); + + return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != (Key)0; } // Mouse @@ -245,6 +257,10 @@ EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const E ev->set_button_mask(mask); input->parse_input_event(ev); + + // Make sure to flush all events so we can call restricted APIs inside the event. + Input::get_singleton()->flush_buffered_events(); + // Prevent multi-click text selection and wheel-click scrolling anchor. // Context menu is prevented through contextmenu event. return true; @@ -481,9 +497,10 @@ EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const Emscript 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(MouseButton(input->get_mouse_button_mask() & ~button_flag)); - input->parse_input_event(ev); + Ref<InputEventMouseButton> release = ev->duplicate(); + release->set_pressed(false); + release->set_button_mask(MouseButton(input->get_mouse_button_mask() & ~button_flag)); + input->parse_input_event(release); return true; } @@ -492,7 +509,6 @@ 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.instantiate(); int lowest_id_index = -1; for (int i = 0; i < p_event->numTouches; ++i) { const EmscriptenTouchPoint &touch = p_event->touches[i]; @@ -500,6 +516,7 @@ EM_BOOL DisplayServerJavaScript::touch_press_callback(int p_event_type, const Em lowest_id_index = i; if (!touch.isChanged) continue; + ev.instantiate(); ev->set_index(touch.identifier); ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); display->touches[i] = ev->get_position(); @@ -507,6 +524,10 @@ EM_BOOL DisplayServerJavaScript::touch_press_callback(int p_event_type, const Em Input::get_singleton()->parse_input_event(ev); } + + // Make sure to flush all events so we can call restricted APIs inside the event. + Input::get_singleton()->flush_buffered_events(); + // Resume audio context after input in case autoplay was denied. return true; } @@ -514,7 +535,6 @@ 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.instantiate(); int lowest_id_index = -1; for (int i = 0; i < p_event->numTouches; ++i) { const EmscriptenTouchPoint &touch = p_event->touches[i]; @@ -522,6 +542,7 @@ EM_BOOL DisplayServerJavaScript::touchmove_callback(int p_event_type, const Emsc lowest_id_index = i; if (!touch.isChanged) continue; + ev.instantiate(); ev->set_index(touch.identifier); ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); Point2 &prev = display->touches[i]; @@ -574,6 +595,12 @@ void DisplayServerJavaScript::virtual_keyboard_hide() { godot_js_display_vk_hide(); } +// Window blur callback +EM_BOOL DisplayServerJavaScript::blur_callback(int p_event_type, const EmscriptenFocusEvent *p_event, void *p_user_data) { + Input::get_singleton()->release_pressed_events(); + return false; +} + // Gamepad void DisplayServerJavaScript::gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid) { Input *input = Input::get_singleton(); @@ -776,6 +803,7 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive SET_EM_CALLBACK(canvas_id, mousedown, mouse_button_callback) SET_EM_WINDOW_CALLBACK(mousemove, mousemove_callback) SET_EM_WINDOW_CALLBACK(mouseup, mouse_button_callback) + SET_EM_WINDOW_CALLBACK(blur, blur_callback) SET_EM_CALLBACK(canvas_id, wheel, wheel_callback) SET_EM_CALLBACK(canvas_id, touchstart, touch_press_callback) SET_EM_CALLBACK(canvas_id, touchmove, touchmove_callback) @@ -1019,6 +1047,7 @@ bool DisplayServerJavaScript::can_any_window_draw() const { } void DisplayServerJavaScript::process_events() { + Input::get_singleton()->flush_buffered_events(); if (godot_js_display_gamepad_sample() == OK) { process_joypads(); } diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index bf5e229c9a..1863ddefeb 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -85,6 +85,8 @@ private: static EM_BOOL touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); static EM_BOOL touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); + static EM_BOOL blur_callback(int p_event_type, const EmscriptenFocusEvent *p_event, void *p_user_data); + static void gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid); void process_joypads(); 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 bf4244eda4..889b0bbd02 100644 --- a/platform/javascript/export/export.cpp +++ b/platform/javascript/export/export.cpp @@ -28,971 +28,9 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#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.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; - } - } -}; - -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(); -}; - -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(); -} +#include "export_plugin.h" void register_javascript_exporter() { EDITOR_DEF("export/web/http_host", "localhost"); 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..c7bd172751 --- /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 (const KeyValue<String, String> &E : p_replaces) { + current_line = current_line.replace(E.key, E.value); + } + 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..bdd1235259 --- /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().to_lower()); + } + + 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/godot_audio.h b/platform/javascript/godot_audio.h index 54fc8fa3b5..eba025ab63 100644 --- a/platform/javascript/godot_audio.h +++ b/platform/javascript/godot_audio.h @@ -38,7 +38,9 @@ extern "C" { #include "stddef.h" extern int godot_audio_is_available(); -extern int godot_audio_init(int p_mix_rate, int p_latency, void (*_state_cb)(int), void (*_latency_cb)(float)); +extern int godot_audio_has_worklet(); +extern int godot_audio_has_script_processor(); +extern int godot_audio_init(int *p_mix_rate, int p_latency, void (*_state_cb)(int), void (*_latency_cb)(float)); extern void godot_audio_resume(); extern int godot_audio_capture_start(); @@ -46,14 +48,15 @@ extern void godot_audio_capture_stop(); // Worklet typedef int32_t GodotAudioState[4]; -extern void godot_audio_worklet_create(int p_channels); +extern int godot_audio_worklet_create(int p_channels); extern void godot_audio_worklet_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, GodotAudioState p_state); +extern void godot_audio_worklet_start_no_threads(float *p_out_buf, int p_out_size, void (*p_out_cb)(int p_pos, int p_frames), float *p_in_buf, int p_in_size, void (*p_in_cb)(int p_pos, int p_frames)); extern int godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value); extern int godot_audio_worklet_state_get(GodotAudioState p_state, int p_idx); extern int godot_audio_worklet_state_wait(int32_t *p_state, int p_idx, int32_t p_expected, int p_timeout); // Script -extern int godot_audio_script_create(int p_buffer_size, int p_channels); +extern int godot_audio_script_create(int *p_buffer_size, int p_channels); extern void godot_audio_script_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, void (*p_cb)()); #ifdef __cplusplus diff --git a/platform/javascript/js/libs/audio.worklet.js b/platform/javascript/js/libs/audio.worklet.js index 866f845139..52b3aedf8c 100644 --- a/platform/javascript/js/libs/audio.worklet.js +++ b/platform/javascript/js/libs/audio.worklet.js @@ -29,15 +29,16 @@ /*************************************************************************/ class RingBuffer { - constructor(p_buffer, p_state) { + constructor(p_buffer, p_state, p_threads) { this.buffer = p_buffer; this.avail = p_state; + this.threads = p_threads; this.rpos = 0; this.wpos = 0; } data_left() { - return Atomics.load(this.avail, 0); + return this.threads ? Atomics.load(this.avail, 0) : this.avail; } space_left() { @@ -55,10 +56,16 @@ class RingBuffer { to_write -= high; this.rpos = 0; } - output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from); + if (to_write) { + output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from); + } this.rpos += to_write; - Atomics.add(this.avail, 0, -output.length); - Atomics.notify(this.avail, 0); + if (this.threads) { + Atomics.add(this.avail, 0, -output.length); + Atomics.notify(this.avail, 0); + } else { + this.avail -= output.length; + } } write(p_buffer) { @@ -66,25 +73,30 @@ class RingBuffer { const mw = this.buffer.length - this.wpos; if (mw >= to_write) { this.buffer.set(p_buffer, this.wpos); + this.wpos += to_write; + if (mw === to_write) { + this.wpos = 0; + } } else { - const high = p_buffer.subarray(0, to_write - mw); - const low = p_buffer.subarray(to_write - mw); + const high = p_buffer.subarray(0, mw); + const low = p_buffer.subarray(mw); this.buffer.set(high, this.wpos); this.buffer.set(low); + this.wpos = low.length; } - let diff = to_write; - if (this.wpos + diff >= this.buffer.length) { - diff -= this.buffer.length; + if (this.threads) { + Atomics.add(this.avail, 0, to_write); + Atomics.notify(this.avail, 0); + } else { + this.avail += to_write; } - this.wpos += diff; - Atomics.add(this.avail, 0, to_write); - Atomics.notify(this.avail, 0); } } class GodotProcessor extends AudioWorkletProcessor { constructor() { super(); + this.threads = false; this.running = true; this.lock = null; this.notifier = null; @@ -100,24 +112,31 @@ class GodotProcessor extends AudioWorkletProcessor { } process_notify() { - Atomics.add(this.notifier, 0, 1); - Atomics.notify(this.notifier, 0); + if (this.notifier) { + Atomics.add(this.notifier, 0, 1); + Atomics.notify(this.notifier, 0); + } } parse_message(p_cmd, p_data) { if (p_cmd === 'start' && p_data) { const state = p_data[0]; let idx = 0; + this.threads = true; this.lock = state.subarray(idx, ++idx); this.notifier = state.subarray(idx, ++idx); const avail_in = state.subarray(idx, ++idx); const avail_out = state.subarray(idx, ++idx); - this.input = new RingBuffer(p_data[1], avail_in); - this.output = new RingBuffer(p_data[2], avail_out); + this.input = new RingBuffer(p_data[1], avail_in, true); + this.output = new RingBuffer(p_data[2], avail_out, true); } else if (p_cmd === 'stop') { this.running = false; this.output = null; this.input = null; + } else if (p_cmd === 'start_nothreads') { + this.output = new RingBuffer(p_data[0], p_data[0].length, false); + } else if (p_cmd === 'chunk') { + this.output.write(p_data); } } @@ -139,7 +158,10 @@ class GodotProcessor extends AudioWorkletProcessor { if (this.input_buffer.length !== chunk) { this.input_buffer = new Float32Array(chunk); } - if (this.input.space_left() >= chunk) { + if (!this.threads) { + GodotProcessor.write_input(this.input_buffer, input); + this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer }); + } else if (this.input.space_left() >= chunk) { GodotProcessor.write_input(this.input_buffer, input); this.input.write(this.input_buffer); } else { @@ -156,6 +178,9 @@ class GodotProcessor extends AudioWorkletProcessor { if (this.output.data_left() >= chunk) { this.output.read(this.output_buffer); GodotProcessor.write_output(output, this.output_buffer); + if (!this.threads) { + this.port.postMessage({ 'cmd': 'read', 'data': chunk }); + } } else { this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); } diff --git a/platform/javascript/js/libs/library_godot_audio.js b/platform/javascript/js/libs/library_godot_audio.js index 45c3a3fe2e..6cbb0567f4 100644 --- a/platform/javascript/js/libs/library_godot_audio.js +++ b/platform/javascript/js/libs/library_godot_audio.js @@ -37,10 +37,14 @@ const GodotAudio = { interval: 0, init: function (mix_rate, latency, onstatechange, onlatencyupdate) { - const ctx = new (window.AudioContext || window.webkitAudioContext)({ - sampleRate: mix_rate, - // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. - }); + const opts = {}; + // If mix_rate is 0, let the browser choose. + if (mix_rate) { + opts['sampleRate'] = mix_rate; + } + // Do not specify, leave 'interactive' for good performance. + // opts['latencyHint'] = latency / 1000; + const ctx = new (window.AudioContext || window.webkitAudioContext)(opts); GodotAudio.ctx = ctx; ctx.onstatechange = function () { let state = 0; @@ -155,11 +159,24 @@ const GodotAudio = { return 1; }, + godot_audio_has_worklet__sig: 'i', + godot_audio_has_worklet: function () { + return (GodotAudio.ctx && GodotAudio.ctx.audioWorklet) ? 1 : 0; + }, + + godot_audio_has_script_processor__sig: 'i', + godot_audio_has_script_processor: function () { + return (GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor) ? 1 : 0; + }, + godot_audio_init__sig: 'iiiii', godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) { const statechange = GodotRuntime.get_func(p_state_change); const latencyupdate = GodotRuntime.get_func(p_latency_update); - return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate); + const mix_rate = GodotRuntime.getHeapValue(p_mix_rate, 'i32'); + const channels = GodotAudio.init(mix_rate, p_latency, statechange, latencyupdate); + GodotRuntime.setHeapValue(p_mix_rate, GodotAudio.ctx.sampleRate, 'i32'); + return channels; }, godot_audio_resume__sig: 'v', @@ -202,6 +219,7 @@ const GodotAudioWorklet = { $GodotAudioWorklet: { promise: null, worklet: null, + ring_buffer: null, create: function (channels) { const path = GodotConfig.locate_file('godot.audio.worklet.js'); @@ -211,7 +229,7 @@ const GodotAudioWorklet = { 'godot-processor', { 'outputChannelCount': [channels], - }, + } ); return Promise.resolve(); }); @@ -232,6 +250,86 @@ const GodotAudioWorklet = { }); }, + start_no_threads: function (p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback) { + function RingBuffer() { + let wpos = 0; + let rpos = 0; + let pending_samples = 0; + const wbuf = new Float32Array(p_out_size); + + function send(port) { + if (pending_samples === 0) { + return; + } + const buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); + const size = buffer.length; + const tot_sent = pending_samples; + out_callback(wpos, pending_samples); + if (wpos + pending_samples >= size) { + const high = size - wpos; + wbuf.set(buffer.subarray(wpos, size)); + pending_samples -= high; + wpos = 0; + } + if (pending_samples > 0) { + wbuf.set(buffer.subarray(wpos, wpos + pending_samples), tot_sent - pending_samples); + } + port.postMessage({ 'cmd': 'chunk', 'data': wbuf.subarray(0, tot_sent) }); + wpos += pending_samples; + pending_samples = 0; + } + this.receive = function (recv_buf) { + const buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); + const from = rpos; + let to_write = recv_buf.length; + let high = 0; + if (rpos + to_write >= p_in_size) { + high = p_in_size - rpos; + buffer.set(recv_buf.subarray(0, high), rpos); + to_write -= high; + rpos = 0; + } + if (to_write) { + buffer.set(recv_buf.subarray(high, to_write), rpos); + } + in_callback(from, recv_buf.length); + rpos += to_write; + }; + this.consumed = function (size, port) { + pending_samples += size; + send(port); + }; + } + GodotAudioWorklet.ring_buffer = new RingBuffer(); + GodotAudioWorklet.promise.then(function () { + const node = GodotAudioWorklet.worklet; + const buffer = GodotRuntime.heapSlice(HEAPF32, p_out_buf, p_out_size); + node.connect(GodotAudio.ctx.destination); + node.port.postMessage({ + 'cmd': 'start_nothreads', + 'data': [buffer, p_in_size], + }); + node.port.onmessage = function (event) { + if (!GodotAudioWorklet.worklet) { + return; + } + if (event.data['cmd'] === 'read') { + const read = event.data['data']; + GodotAudioWorklet.ring_buffer.consumed(read, GodotAudioWorklet.worklet.port); + } else if (event.data['cmd'] === 'input') { + const buf = event.data['data']; + if (buf.length > p_in_size) { + GodotRuntime.error('Input chunk is too big'); + return; + } + GodotAudioWorklet.ring_buffer.receive(buf); + } else { + GodotRuntime.error(event.data); + } + }; + }); + }, + get_node: function () { return GodotAudioWorklet.worklet; }, @@ -255,9 +353,15 @@ const GodotAudioWorklet = { }, }, - godot_audio_worklet_create__sig: 'vi', + godot_audio_worklet_create__sig: 'ii', godot_audio_worklet_create: function (channels) { - GodotAudioWorklet.create(channels); + try { + GodotAudioWorklet.create(channels); + } catch (e) { + GodotRuntime.error('Error starting AudioDriverWorklet', e); + return 1; + } + return 0; }, godot_audio_worklet_start__sig: 'viiiii', @@ -268,6 +372,13 @@ const GodotAudioWorklet = { GodotAudioWorklet.start(in_buffer, out_buffer, state); }, + godot_audio_worklet_start_no_threads__sig: 'viiiiii', + godot_audio_worklet_start_no_threads: function (p_out_buf, p_out_size, p_out_callback, p_in_buf, p_in_size, p_in_callback) { + const out_callback = GodotRuntime.get_func(p_out_callback); + const in_callback = GodotRuntime.get_func(p_in_callback); + GodotAudioWorklet.start_no_threads(p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback); + }, + godot_audio_worklet_state_wait__sig: 'iiii', godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) { Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout); @@ -351,7 +462,15 @@ const GodotAudioScript = { godot_audio_script_create__sig: 'iii', godot_audio_script_create: function (buffer_length, channel_count) { - return GodotAudioScript.create(buffer_length, channel_count); + const buf_len = GodotRuntime.getHeapValue(buffer_length, 'i32'); + try { + const out_len = GodotAudioScript.create(buf_len, channel_count); + GodotRuntime.setHeapValue(buffer_length, out_len, 'i32'); + } catch (e) { + GodotRuntime.error('Error starting AudioDriverScriptProcessor', e); + return 1; + } + return 0; }, godot_audio_script_start__sig: 'viiiii', diff --git a/platform/javascript/js/libs/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js index 5aa750757c..99e7ee8b5f 100644 --- a/platform/javascript/js/libs/library_godot_os.js +++ b/platform/javascript/js/libs/library_godot_os.js @@ -106,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 76102d941b..4431bd5f1b 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -63,9 +63,7 @@ void OS_JavaScript::initialize() { } void OS_JavaScript::resume_audio() { - if (audio_driver_javascript) { - audio_driver_javascript->resume(); - } + AudioDriverJavaScript::resume(); } void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) { @@ -101,10 +99,10 @@ void OS_JavaScript::delete_main_loop() { void OS_JavaScript::finalize() { delete_main_loop(); - if (audio_driver_javascript) { - memdelete(audio_driver_javascript); - audio_driver_javascript = nullptr; + for (AudioDriverJavaScript *driver : audio_drivers) { + memdelete(driver); } + audio_drivers.clear(); } // Miscellaneous @@ -137,12 +135,12 @@ int OS_JavaScript::get_processor_count() const { } bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) { - if (p_feature == "HTML5" || p_feature == "web") { + if (p_feature == "html5" || p_feature == "web") { return true; } #ifdef JAVASCRIPT_EVAL_ENABLED - if (p_feature == "JavaScript") { + if (p_feature == "javascript") { return true; } #endif @@ -229,8 +227,13 @@ OS_JavaScript::OS_JavaScript() { setenv("LANG", locale_ptr, true); if (AudioDriverJavaScript::is_available()) { - audio_driver_javascript = memnew(AudioDriverJavaScript); - AudioDriverManager::add_driver(audio_driver_javascript); +#ifdef NO_THREADS + audio_drivers.push_back(memnew(AudioDriverScriptProcessor)); +#endif + audio_drivers.push_back(memnew(AudioDriverWorklet)); + } + for (int i = 0; i < audio_drivers.size(); i++) { + AudioDriverManager::add_driver(audio_drivers[i]); } idb_available = godot_js_os_fs_is_persistent(); diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index efac2dbca7..d053082d92 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -40,7 +40,7 @@ class OS_JavaScript : public OS_Unix { MainLoop *main_loop = nullptr; - AudioDriverJavaScript *audio_driver_javascript = nullptr; + List<AudioDriverJavaScript *> audio_drivers; bool idb_is_syncing = false; bool idb_available = false; |