feat: updated engine version to 4.4-rc1

This commit is contained in:
Sara 2025-02-23 14:38:14 +01:00
parent ee00efde1f
commit 21ba8e33af
5459 changed files with 1128836 additions and 198305 deletions

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python
from misc.utility.scons_hints import *
from methods import print_error
@ -21,20 +22,26 @@ if "serve" in COMMAND_LINE_TARGETS or "run" in COMMAND_LINE_TARGETS:
web_files = [
"audio_driver_web.cpp",
"webmidi_driver.cpp",
"display_server_web.cpp",
"http_client_web.cpp",
"javascript_bridge_singleton.cpp",
"web_main.cpp",
"ip_web.cpp",
"net_socket_web.cpp",
"os_web.cpp",
"api/web_tools_editor_plugin.cpp",
]
if env["target"] == "editor":
env.add_source_files(web_files, "editor/*.cpp")
sys_env = env.Clone()
sys_env.AddJSLibraries(
[
"js/libs/library_godot_audio.js",
"js/libs/library_godot_display.js",
"js/libs/library_godot_fetch.js",
"js/libs/library_godot_webmidi.js",
"js/libs/library_godot_os.js",
"js/libs/library_godot_runtime.js",
"js/libs/library_godot_input.js",
@ -58,7 +65,7 @@ for ext in sys_env["JS_EXTERNS"]:
sys_env["ENV"]["EMCC_CLOSURE_ARGS"] += " --externs " + ext.abspath
build = []
build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm", "#bin/godot${PROGSUFFIX}.worker.js"]
build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"]
if env["dlink_enabled"]:
# Reset libraries. The main runtime will only link emscripten libraries, not godot ones.
sys_env["LIBS"] = []
@ -107,6 +114,5 @@ js_wrapped = env.Textfile("#bin/godot", [env.File(f) for f in wrap_list], TEXTFI
# 0 - unwrapped js file (use wrapped one instead)
# 1 - wasm file
# 2 - worker file
# 3 - wasm side (when dlink is enabled).
env.CreateTemplateZip(js_wrapped, build[1], build[2], build[3] if len(build) > 3 else None)
# 2 - wasm side (when dlink is enabled).
env.CreateTemplateZip(js_wrapped, build[1], build[2] if len(build) > 2 else None)

View file

@ -31,14 +31,12 @@
#include "api.h"
#include "javascript_bridge_singleton.h"
#include "web_tools_editor_plugin.h"
#include "core/config/engine.h"
static JavaScriptBridge *javascript_bridge_singleton;
void register_web_api() {
WebToolsEditorPlugin::initialize();
GDREGISTER_ABSTRACT_CLASS(JavaScriptObject);
GDREGISTER_ABSTRACT_CLASS(JavaScriptBridge);
javascript_bridge_singleton = memnew(JavaScriptBridge);
@ -66,6 +64,8 @@ void JavaScriptBridge::_bind_methods() {
ClassDB::bind_method(D_METHOD("eval", "code", "use_global_execution_context"), &JavaScriptBridge::eval, DEFVAL(false));
ClassDB::bind_method(D_METHOD("get_interface", "interface"), &JavaScriptBridge::get_interface);
ClassDB::bind_method(D_METHOD("create_callback", "callable"), &JavaScriptBridge::create_callback);
ClassDB::bind_method(D_METHOD("is_js_buffer", "javascript_object"), &JavaScriptBridge::is_js_buffer);
ClassDB::bind_method(D_METHOD("js_buffer_to_packed_byte_array", "javascript_buffer"), &JavaScriptBridge::js_buffer_to_packed_byte_array);
{
MethodInfo mi;
mi.name = "create_object";
@ -93,13 +93,21 @@ Ref<JavaScriptObject> JavaScriptBridge::create_callback(const Callable &p_callab
return Ref<JavaScriptObject>();
}
bool JavaScriptBridge::is_js_buffer(Ref<JavaScriptObject> p_js_obj) {
return false;
}
PackedByteArray JavaScriptBridge::js_buffer_to_packed_byte_array(Ref<JavaScriptObject> p_js_obj) {
return PackedByteArray();
}
Variant JavaScriptBridge::_create_object_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
if (p_argcount < 1) {
r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS;
r_error.expected = 1;
return Ref<JavaScriptObject>();
}
if (p_args[0]->get_type() != Variant::STRING) {
if (!p_args[0]->is_string()) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::STRING;

View file

@ -57,6 +57,8 @@ public:
Variant eval(const String &p_code, bool p_use_global_exec_context = false);
Ref<JavaScriptObject> get_interface(const String &p_interface);
Ref<JavaScriptObject> create_callback(const Callable &p_callable);
bool is_js_buffer(Ref<JavaScriptObject> p_js_obj);
PackedByteArray js_buffer_to_packed_byte_array(Ref<JavaScriptObject> p_js_obj);
Variant _create_object_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error);
void download_buffer(Vector<uint8_t> p_arr, const String &p_name, const String &p_mime = "application/octet-stream");
bool pwa_needs_update() const;

View file

@ -294,6 +294,7 @@ void AudioDriverWeb::start_sample_playback(const Ref<AudioSamplePlayback> &p_pla
itos(p_playback->stream->get_instance_id()).utf8().get_data(),
AudioServer::get_singleton()->get_bus_index(p_playback->bus),
p_playback->offset,
p_playback->pitch_scale,
volume_ptrw);
}
@ -312,6 +313,11 @@ bool AudioDriverWeb::is_sample_playback_active(const Ref<AudioSamplePlayback> &p
return godot_audio_sample_is_active(itos(p_playback->get_instance_id()).utf8().get_data()) != 0;
}
double AudioDriverWeb::get_sample_playback_position(const Ref<AudioSamplePlayback> &p_playback) {
ERR_FAIL_COND_V_MSG(p_playback.is_null(), false, "Parameter p_playback is null.");
return godot_audio_get_sample_playback_position(itos(p_playback->get_instance_id()).utf8().get_data());
}
void AudioDriverWeb::update_sample_playback_pitch_scale(const Ref<AudioSamplePlayback> &p_playback, float p_pitch_scale) {
ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null.");
godot_audio_sample_update_pitch_scale(
@ -473,6 +479,8 @@ void AudioDriverWorklet::_capture_callback(int p_pos, int p_samples) {
driver->_audio_driver_capture(p_pos, p_samples);
}
#endif // THREADS_ENABLED
/// ScriptProcessorNode implementation
AudioDriverScriptProcessor *AudioDriverScriptProcessor::singleton = nullptr;
@ -491,5 +499,3 @@ Error AudioDriverScriptProcessor::create(int &p_buffer_samples, int p_channels)
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);
}
#endif // THREADS_ENABLED

View file

@ -96,6 +96,7 @@ public:
virtual void stop_sample_playback(const Ref<AudioSamplePlayback> &p_playback) override;
virtual void set_sample_playback_pause(const Ref<AudioSamplePlayback> &p_playback, bool p_paused) override;
virtual bool is_sample_playback_active(const Ref<AudioSamplePlayback> &p_playback) override;
virtual double get_sample_playback_position(const Ref<AudioSamplePlayback> &p_playback) override;
virtual void update_sample_playback_pitch_scale(const Ref<AudioSamplePlayback> &p_playback, float p_pitch_scale = 0.0f) override;
virtual void set_sample_playback_bus_volumes_linear(const Ref<AudioSamplePlayback> &p_playback, const HashMap<StringName, Vector<AudioFrame>> &p_bus_volumes) override;
@ -168,6 +169,8 @@ public:
AudioDriverWorklet() { singleton = this; }
};
#endif // THREADS_ENABLED
class AudioDriverScriptProcessor : public AudioDriverWeb {
private:
static void _process_callback();
@ -177,7 +180,6 @@ private:
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 "ScriptProcessor"; }
@ -190,6 +192,4 @@ public:
AudioDriverScriptProcessor() { singleton = this; }
};
#endif // THREADS_ENABLED
#endif // AUDIO_DRIVER_WEB_H

View file

@ -13,7 +13,8 @@ from emscripten_helpers import (
)
from SCons.Util import WhereIs
from methods import get_compiler_version, print_error, print_warning
from methods import get_compiler_version, print_error, print_info, print_warning
from platform_methods import validate_arch
if TYPE_CHECKING:
from SCons.Script.SConscript import SConsEnvironment
@ -27,6 +28,11 @@ def can_build():
return WhereIs("emcc") is not None
def get_tools(env: "SConsEnvironment"):
# Use generic POSIX build toolchain for Emscripten.
return ["cc", "c++", "ar", "link", "textfile", "zip"]
def get_opts():
from SCons.Variables import BoolVariable
@ -86,12 +92,7 @@ def get_flags():
def configure(env: "SConsEnvironment"):
# Validate arch.
supported_arches = ["wasm32"]
if env["arch"] not in supported_arches:
print_error(
'Unsupported CPU architecture "%s" for Web. Supported architectures are: %s.'
% (env["arch"], ", ".join(supported_arches))
)
sys.exit(255)
validate_arch(env["arch"], get_name(), supported_arches)
try:
env["initial_memory"] = int(env["initial_memory"])
@ -111,7 +112,7 @@ def configure(env: "SConsEnvironment"):
env.Append(LINKFLAGS=["-sASSERTIONS=1"])
if env.editor_build and env["initial_memory"] < 64:
print("Note: Forcing `initial_memory=64` as it is required for the web editor.")
print_info("Forcing `initial_memory=64` as it is required for the web editor.")
env["initial_memory"] = 64
env.Append(LINKFLAGS=["-sINITIAL_MEMORY=%sMB" % env["initial_memory"]])
@ -121,8 +122,8 @@ def configure(env: "SConsEnvironment"):
# LTO
if env["lto"] == "auto": # Full LTO for production.
env["lto"] = "full"
if env["lto"] == "auto": # Enable LTO for production.
env["lto"] = "thin"
if env["lto"] != "none":
if env["lto"] == "thin":
@ -199,8 +200,13 @@ def configure(env: "SConsEnvironment"):
cc_version = get_compiler_version(env)
cc_semver = (cc_version["major"], cc_version["minor"], cc_version["patch"])
# Minimum emscripten requirements.
if cc_semver < (3, 1, 62):
print_error("The minimum emscripten version to build Godot is 3.1.62, detected: %s.%s.%s" % cc_semver)
sys.exit(255)
env.Prepend(CPPPATH=["#platform/web"])
env.Append(CPPDEFINES=["WEB_ENABLED", "UNIX_ENABLED"])
env.Append(CPPDEFINES=["WEB_ENABLED", "UNIX_ENABLED", "UNIX_SOCKET_UNAVAILABLE"])
if env["opengl3"]:
env.AppendUnique(CPPDEFINES=["GLES3_ENABLED"])
@ -210,14 +216,12 @@ def configure(env: "SConsEnvironment"):
env.Append(LINKFLAGS=["-sOFFSCREEN_FRAMEBUFFER=1"])
# Disables the use of *glGetProcAddress() which is inefficient.
# See https://emscripten.org/docs/tools_reference/settings_reference.html#gl-enable-get-proc-address
if cc_semver >= (3, 1, 51):
env.Append(LINKFLAGS=["-sGL_ENABLE_GET_PROC_ADDRESS=0"])
env.Append(LINKFLAGS=["-sGL_ENABLE_GET_PROC_ADDRESS=0"])
if env["javascript_eval"]:
env.Append(CPPDEFINES=["JAVASCRIPT_EVAL_ENABLED"])
stack_size_opt = "STACK_SIZE" if cc_semver >= (3, 1, 25) else "TOTAL_STACK"
env.Append(LINKFLAGS=["-s%s=%sKB" % (stack_size_opt, env["stack_size"])])
env.Append(LINKFLAGS=["-s%s=%sKB" % ("STACK_SIZE", env["stack_size"])])
if env["threads"]:
# Thread support (via SharedArrayBuffer).
@ -237,30 +241,21 @@ def configure(env: "SConsEnvironment"):
env["proxy_to_pthread"] = False
if env["lto"] != "none":
# Workaround https://github.com/emscripten-core/emscripten/issues/19781.
if cc_semver >= (3, 1, 42) and cc_semver < (3, 1, 46):
env.Append(LINKFLAGS=["-Wl,-u,scalbnf"])
# Workaround https://github.com/emscripten-core/emscripten/issues/16836.
if cc_semver >= (3, 1, 47):
env.Append(LINKFLAGS=["-Wl,-u,_emscripten_run_callback_on_thread"])
env.Append(LINKFLAGS=["-Wl,-u,_emscripten_run_callback_on_thread"])
if env["dlink_enabled"]:
if env["proxy_to_pthread"]:
print_warning("GDExtension support requires proxy_to_pthread=no, disabling proxy to pthread.")
env["proxy_to_pthread"] = False
if cc_semver < (3, 1, 14):
print_error("GDExtension support requires emscripten >= 3.1.14, detected: %s.%s.%s" % cc_semver)
sys.exit(255)
env.Append(CCFLAGS=["-sSIDE_MODULE=2"])
env.Append(LINKFLAGS=["-sSIDE_MODULE=2"])
env.Append(CCFLAGS=["-fvisibility=hidden"])
env.Append(LINKFLAGS=["-fvisibility=hidden"])
env.extra_suffix = ".dlink" + env.extra_suffix
# WASM_BIGINT is needed since emscripten ≥ 3.1.41
needs_wasm_bigint = cc_semver >= (3, 1, 41)
env.Append(LINKFLAGS=["-sWASM_BIGINT"])
# Run the main application in a web worker
if env["proxy_to_pthread"]:
@ -269,11 +264,6 @@ def configure(env: "SConsEnvironment"):
env.Append(LINKFLAGS=["-sEXPORTED_RUNTIME_METHODS=['_emscripten_proxy_main']"])
# https://github.com/emscripten-core/emscripten/issues/18034#issuecomment-1277561925
env.Append(LINKFLAGS=["-sTEXTDECODER=0"])
# BigInt support to pass object pointers between contexts
needs_wasm_bigint = True
if needs_wasm_bigint:
env.Append(LINKFLAGS=["-sWASM_BIGINT"])
# Reduce code size by generating less support code (e.g. skip NodeJS support).
env.Append(LINKFLAGS=["-sENVIRONMENT=web,worker"])

View file

@ -550,26 +550,47 @@ void DisplayServerWeb::cursor_set_custom_image(const Ref<Resource> &p_cursor, Cu
}
// Mouse mode
void DisplayServerWeb::mouse_set_mode(MouseMode p_mode) {
ERR_FAIL_COND_MSG(p_mode == MOUSE_MODE_CONFINED || p_mode == MOUSE_MODE_CONFINED_HIDDEN, "MOUSE_MODE_CONFINED is not supported for the Web platform.");
if (p_mode == mouse_get_mode()) {
void DisplayServerWeb::_mouse_update_mode() {
MouseMode wanted_mouse_mode = mouse_mode_override_enabled
? mouse_mode_override
: mouse_mode_base;
ERR_FAIL_COND_MSG(wanted_mouse_mode == MOUSE_MODE_CONFINED || wanted_mouse_mode == MOUSE_MODE_CONFINED_HIDDEN, "MOUSE_MODE_CONFINED is not supported for the Web platform.");
if (wanted_mouse_mode == mouse_get_mode()) {
return;
}
if (p_mode == MOUSE_MODE_VISIBLE) {
if (wanted_mouse_mode == MOUSE_MODE_VISIBLE) {
godot_js_display_cursor_set_visible(1);
godot_js_display_cursor_lock_set(0);
} else if (p_mode == MOUSE_MODE_HIDDEN) {
} else if (wanted_mouse_mode == MOUSE_MODE_HIDDEN) {
godot_js_display_cursor_set_visible(0);
godot_js_display_cursor_lock_set(0);
} else if (p_mode == MOUSE_MODE_CAPTURED) {
} else if (wanted_mouse_mode == MOUSE_MODE_CAPTURED) {
godot_js_display_cursor_set_visible(1);
godot_js_display_cursor_lock_set(1);
}
}
void DisplayServerWeb::mouse_set_mode(MouseMode p_mode) {
ERR_FAIL_INDEX(p_mode, MouseMode::MOUSE_MODE_MAX);
if (mouse_mode_override_enabled) {
mouse_mode_base = p_mode;
// No need to update, as override is enabled.
return;
}
if (p_mode == mouse_mode_base && p_mode == mouse_get_mode()) {
// No need to update, as it is currently set as the correct mode.
return;
}
mouse_mode_base = p_mode;
_mouse_update_mode();
}
DisplayServer::MouseMode DisplayServerWeb::mouse_get_mode() const {
if (godot_js_display_cursor_is_hidden()) {
return MOUSE_MODE_HIDDEN;
@ -581,6 +602,39 @@ DisplayServer::MouseMode DisplayServerWeb::mouse_get_mode() const {
return MOUSE_MODE_VISIBLE;
}
void DisplayServerWeb::mouse_set_mode_override(MouseMode p_mode) {
ERR_FAIL_INDEX(p_mode, MouseMode::MOUSE_MODE_MAX);
if (!mouse_mode_override_enabled) {
mouse_mode_override = p_mode;
// No need to update, as override is not enabled.
return;
}
if (p_mode == mouse_mode_override && p_mode == mouse_get_mode()) {
// No need to update, as it is currently set as the correct mode.
return;
}
mouse_mode_override = p_mode;
_mouse_update_mode();
}
DisplayServer::MouseMode DisplayServerWeb::mouse_get_mode_override() const {
return mouse_mode_override;
}
void DisplayServerWeb::mouse_set_mode_override_enabled(bool p_override_enabled) {
if (p_override_enabled == mouse_mode_override_enabled) {
return;
}
mouse_mode_override_enabled = p_override_enabled;
_mouse_update_mode();
}
bool DisplayServerWeb::mouse_is_mode_override_enabled() const {
return mouse_mode_override_enabled;
}
Point2i DisplayServerWeb::mouse_get_position() const {
return Input::get_singleton()->get_mouse_position();
}
@ -902,8 +956,10 @@ void DisplayServerWeb::process_joypads() {
for (int b = 0; b < s_btns_num; b++) {
// Buttons 6 and 7 in the standard mapping need to be
// axis to be handled as JoyAxis::TRIGGER by Godot.
if (s_standard && (b == 6 || b == 7)) {
input->joy_axis(idx, (JoyAxis)b, s_btns[b]);
if (s_standard && (b == 6)) {
input->joy_axis(idx, JoyAxis::TRIGGER_LEFT, s_btns[b]);
} else if (s_standard && (b == 7)) {
input->joy_axis(idx, JoyAxis::TRIGGER_RIGHT, s_btns[b]);
} else {
input->joy_button(idx, (JoyButton)b, s_btns[b]);
}
@ -1023,11 +1079,11 @@ void DisplayServerWeb::_dispatch_input_event(const Ref<InputEvent> &p_event) {
}
}
DisplayServer *DisplayServerWeb::create_func(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Point2i *p_position, const Size2i &p_resolution, int p_screen, Context p_context, Error &r_error) {
return memnew(DisplayServerWeb(p_rendering_driver, p_window_mode, p_vsync_mode, p_flags, p_position, p_resolution, p_screen, p_context, r_error));
DisplayServer *DisplayServerWeb::create_func(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Point2i *p_position, const Size2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error) {
return memnew(DisplayServerWeb(p_rendering_driver, p_window_mode, p_vsync_mode, p_flags, p_position, p_resolution, p_screen, p_context, p_parent_window, r_error));
}
DisplayServerWeb::DisplayServerWeb(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Point2i *p_position, const Size2i &p_resolution, int p_screen, Context p_context, Error &r_error) {
DisplayServerWeb::DisplayServerWeb(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Point2i *p_position, const Size2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error) {
r_error = OK; // Always succeeds for now.
tts = GLOBAL_GET("audio/general/text_to_speech");
@ -1131,6 +1187,8 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const {
//case FEATURE_NATIVE_DIALOG:
//case FEATURE_NATIVE_DIALOG_INPUT:
//case FEATURE_NATIVE_DIALOG_FILE:
//case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
//case FEATURE_NATIVE_DIALOG_FILE_MIME:
//case FEATURE_NATIVE_ICON:
//case FEATURE_WINDOW_TRANSPARENCY:
//case FEATURE_KEEP_SCREEN_ON:

View file

@ -106,6 +106,11 @@ private:
bool tts = false;
NativeMenu *native_menu = nullptr;
MouseMode mouse_mode_base = MOUSE_MODE_VISIBLE;
MouseMode mouse_mode_override = MOUSE_MODE_VISIBLE;
bool mouse_mode_override_enabled = false;
void _mouse_update_mode();
// utilities
static void dom2godot_mod(Ref<InputEventWithModifiers> ev, int p_mod, Key p_keycode);
static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape);
@ -148,7 +153,7 @@ private:
void process_keys();
static Vector<String> get_rendering_drivers_func();
static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error);
static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error);
static void _dispatch_input_event(const Ref<InputEvent> &p_event);
@ -184,6 +189,11 @@ public:
// mouse
virtual void mouse_set_mode(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode() const override;
virtual void mouse_set_mode_override(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode_override() const override;
virtual void mouse_set_mode_override_enabled(bool p_override_enabled) override;
virtual bool mouse_is_mode_override_enabled() const override;
virtual Point2i mouse_get_position() const override;
// ime
@ -209,6 +219,7 @@ public:
virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
virtual void screen_set_keep_on(bool p_enable) override {}
virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), VirtualKeyboardType p_type = KEYBOARD_TYPE_DEFAULT, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override;
virtual void virtual_keyboard_hide() override;
@ -265,6 +276,7 @@ public:
virtual bool can_any_window_draw() const override;
virtual void window_set_vsync_mode(VSyncMode p_vsync_mode, WindowID p_window = MAIN_WINDOW_ID) override {}
virtual DisplayServer::VSyncMode window_get_vsync_mode(WindowID p_vsync_mode) const override;
// events
@ -278,7 +290,7 @@ public:
virtual void swap_buffers() override;
static void register_web_driver();
DisplayServerWeb(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Point2i *p_position, const Size2i &p_resolution, int p_screen, Context p_context, Error &r_error);
DisplayServerWeb(const String &p_rendering_driver, WindowMode p_window_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Point2i *p_position, const Size2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error);
~DisplayServerWeb();
};

View file

@ -60,15 +60,15 @@
</member>
<member name="progressive_web_app/icon_144x144" type="String" setter="" getter="">
File path to the smallest icon for this web application. If not defined, defaults to the project icon.
[b]Note:[/b] If the icon is not 144x144, it will be automatically resized for the final build.
[b]Note:[/b] If the icon is not 144×144, it will be automatically resized for the final build.
</member>
<member name="progressive_web_app/icon_180x180" type="String" setter="" getter="">
File path to the small icon for this web application. If not defined, defaults to the project icon.
[b]Note:[/b] If the icon is not 180x180, it will be automatically resized for the final build.
[b]Note:[/b] If the icon is not 180×180, it will be automatically resized for the final build.
</member>
<member name="progressive_web_app/icon_512x512" type="String" setter="" getter="">
File path to the smallest icon for this web application. If not defined, defaults to the project icon.
[b]Note:[/b] If the icon is not 512x512, it will be automatically resized for the final build.
File path to the largest icon for this web application. If not defined, defaults to the project icon.
[b]Note:[/b] If the icon is not 512×512, it will be automatically resized for the final build.
</member>
<member name="progressive_web_app/offline_page" type="String" setter="" getter="">
The page to display, should the server hosting the page not be available. This page is saved in the client's machine.
@ -87,10 +87,10 @@
If [code]false[/code], the exported game will not support threads. As a result, it is more prone to performance and audio issues, but will only require to be run on an HTTPS website.
</member>
<member name="vram_texture_compression/for_desktop" type="bool" setter="" getter="">
If [code]true[/code], allows textures to be optimized for desktop through the S3TC algorithm.
If [code]true[/code], allows textures to be optimized for desktop through the S3TC/BPTC algorithm.
</member>
<member name="vram_texture_compression/for_mobile" type="bool" setter="" getter="">
If [code]true[/code] allows textures to be optimized for mobile through the ETC2 algorithm.
If [code]true[/code] allows textures to be optimized for mobile through the ETC2/ASTC algorithm.
</member>
</members>
</class>

View file

@ -30,14 +30,13 @@
#include "web_tools_editor_plugin.h"
#if defined(TOOLS_ENABLED) && defined(WEB_ENABLED)
#include "core/config/engine.h"
#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 "editor/export/project_zip_packer.h"
#include <emscripten/emscripten.h>
@ -63,26 +62,10 @@ void WebToolsEditorPlugin::_download_zip() {
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();
Ref<FileAccess> io_fa;
zlib_filefunc_def io = zipio_create_io(&io_fa);
// Name the downloaded 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", project_name_safe, datetime_safe));
const String output_name = ProjectZIPPacker::get_project_zip_safe_name();
const String output_path = String("/tmp").path_join(output_name);
ProjectZIPPacker::pack_project_zip(output_path);
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);
{
Ref<FileAccess> f = FileAccess::open(output_path, FileAccess::READ);
ERR_FAIL_COND_MSG(f.is_null(), "Unable to create ZIP file.");
@ -95,65 +78,3 @@ void WebToolsEditorPlugin::_download_zip() {
// 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 WebToolsEditorPlugin::_zip_file(String p_path, String p_base_path, zipFile p_zip) {
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
if (f.is_null()) {
WARN_PRINT("Unable to open file for zipping: " + p_path);
return;
}
Vector<uint8_t> data;
uint64_t len = f->get_length();
data.resize(len);
f->get_buffer(data.ptrw(), len);
String path = p_path.replace_first(p_base_path, "");
zipOpenNewFileInZip(p_zip,
path.utf8().get_data(),
nullptr,
nullptr,
0,
nullptr,
0,
nullptr,
Z_DEFLATED,
Z_DEFAULT_COMPRESSION);
zipWriteInFileInZip(p_zip, data.ptr(), data.size());
zipCloseFileInZip(p_zip);
}
void WebToolsEditorPlugin::_zip_recursive(String p_path, String p_base_path, zipFile p_zip) {
Ref<DirAccess> dir = DirAccess::open(p_path);
if (dir.is_null()) {
WARN_PRINT("Unable to open directory for zipping: " + p_path);
return;
}
dir->list_dir_begin();
String cur = dir->get_next();
String project_data_dir_name = ProjectSettings::get_singleton()->get_project_data_dir_name();
while (!cur.is_empty()) {
String cs = p_path.path_join(cur);
if (cur == "." || cur == ".." || cur == project_data_dir_name) {
// Skip
} else if (dir->current_is_dir()) {
String path = cs.replace_first(p_base_path, "") + "/";
zipOpenNewFileInZip(p_zip,
path.utf8().get_data(),
nullptr,
nullptr,
0,
nullptr,
0,
nullptr,
Z_DEFLATED,
Z_DEFAULT_COMPRESSION);
zipCloseFileInZip(p_zip);
_zip_recursive(cs, p_base_path, p_zip);
} else {
_zip_file(cs, p_base_path, p_zip);
}
cur = dir->get_next();
}
}
#endif // TOOLS_ENABLED && WEB_ENABLED

View file

@ -31,8 +31,6 @@
#ifndef WEB_TOOLS_EDITOR_PLUGIN_H
#define WEB_TOOLS_EDITOR_PLUGIN_H
#if defined(TOOLS_ENABLED) && defined(WEB_ENABLED)
#include "core/io/zip_io.h"
#include "editor/plugins/editor_plugin.h"
@ -40,8 +38,6 @@ class WebToolsEditorPlugin : public EditorPlugin {
GDCLASS(WebToolsEditorPlugin, EditorPlugin);
private:
void _zip_file(String p_path, String p_base_path, zipFile p_zip);
void _zip_recursive(String p_path, String p_base_path, zipFile p_zip);
void _download_zip();
public:
@ -57,6 +53,4 @@ public:
static void initialize() {}
};
#endif // TOOLS_ENABLED && WEB_ENABLED
#endif // WEB_TOOLS_EDITOR_PLUGIN_H

View file

@ -3,6 +3,8 @@ import os
from SCons.Util import WhereIs
from platform_methods import get_build_version
def run_closure_compiler(target, source, env, for_signature):
closure_bin = os.path.join(
@ -21,22 +23,6 @@ def run_closure_compiler(target, source, env, for_signature):
return " ".join(cmd)
def get_build_version():
import version
name = "custom_build"
if os.getenv("BUILD_NAME") is not None:
name = os.getenv("BUILD_NAME")
v = "%d.%d" % (version.major, version.minor)
if version.patch > 0:
v += ".%d" % version.patch
status = version.status
if os.getenv("GODOT_VERSION_STATUS") is not None:
status = str(os.getenv("GODOT_VERSION_STATUS"))
v += ".%s.%s" % (status, name)
return v
def create_engine_file(env, target, source, externs, threads_enabled):
if env["use_closure_compiler"]:
return env.BuildJS(target, source, JSEXTERNS=externs)
@ -44,22 +30,21 @@ def create_engine_file(env, target, source, externs, threads_enabled):
return env.Substfile(target=target, source=[env.File(s) for s in source], SUBST_DICT=subst_dict)
def create_template_zip(env, js, wasm, worker, side):
def create_template_zip(env, js, wasm, side):
binary_name = "godot.editor" if env.editor_build else "godot"
zip_dir = env.Dir(env.GetTemplateZipPath())
in_files = [
js,
wasm,
"#platform/web/js/libs/audio.worklet.js",
"#platform/web/js/libs/audio.position.worklet.js",
]
out_files = [
zip_dir.File(binary_name + ".js"),
zip_dir.File(binary_name + ".wasm"),
zip_dir.File(binary_name + ".audio.worklet.js"),
zip_dir.File(binary_name + ".audio.position.worklet.js"),
]
if env["threads"]:
in_files.append(worker)
out_files.append(zip_dir.File(binary_name + ".worker.js"))
# Dynamic linking (extensions) specific.
if env["dlink_enabled"]:
in_files.append(side) # Side wasm (contains the actual Godot code).
@ -74,19 +59,19 @@ def create_template_zip(env, js, wasm, worker, side):
"offline.html",
"godot.editor.js",
"godot.editor.audio.worklet.js",
"godot.editor.audio.position.worklet.js",
"logo.svg",
"favicon.png",
]
if env["threads"]:
cache.append("godot.editor.worker.js")
opt_cache = ["godot.editor.wasm"]
subst_dict = {
"___GODOT_VERSION___": get_build_version(),
"___GODOT_VERSION___": get_build_version(False),
"___GODOT_NAME___": "GodotEngine",
"___GODOT_CACHE___": json.dumps(cache),
"___GODOT_OPT_CACHE___": json.dumps(opt_cache),
"___GODOT_OFFLINE_PAGE___": "offline.html",
"___GODOT_THREADS_ENABLED___": "true" if env["threads"] else "false",
"___GODOT_ENSURE_CROSSORIGIN_ISOLATION_HEADERS___": "true",
}
html = env.Substfile(target="#bin/godot${PROGSUFFIX}.html", source=html, SUBST_DICT=subst_dict)
in_files.append(html)

View file

@ -86,7 +86,7 @@ void EditorHTTPServer::_send_response() {
const String req_file = path.get_file();
const String req_ext = path.get_extension();
const String cache_path = EditorPaths::get_singleton()->get_cache_dir().path_join("web");
const String cache_path = EditorPaths::get_singleton()->get_temp_dir().path_join("web");
const String filepath = cache_path.path_join(req_file);
if (!mimes.has(req_ext) || !FileAccess::exists(filepath)) {

View file

@ -40,7 +40,7 @@ void register_web_exporter_types() {
}
void register_web_exporter() {
#ifndef ANDROID_ENABLED
// TODO: Move to editor_settings.cpp
EDITOR_DEF("export/web/http_host", "localhost");
EDITOR_DEF("export/web/http_port", 8060);
EDITOR_DEF("export/web/use_tls", false);
@ -49,7 +49,6 @@ void register_web_exporter() {
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "export/web/http_port", PROPERTY_HINT_RANGE, "1,65535,1"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/tls_key", PROPERTY_HINT_GLOBAL_FILE, "*.key"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/tls_certificate", PROPERTY_HINT_GLOBAL_FILE, "*.crt,*.pem"));
#endif
Ref<EditorExportPlatformWeb> platform;
platform.instantiate();

View file

@ -41,10 +41,8 @@
#include "editor/themes/editor_scale.h"
#include "scene/resources/image_texture.h"
#include "modules/modules_enabled.gen.h" // For mono and svg.
#ifdef MODULE_SVG_ENABLED
#include "modules/modules_enabled.gen.h" // For mono.
#include "modules/svg/image_loader_svg.h"
#endif
Error EditorExportPlatformWeb::_extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa) {
Ref<FileAccess> io_fa;
@ -130,15 +128,14 @@ void EditorExportPlatformWeb::_replace_strings(const HashMap<String, String> &p_
}
}
void EditorExportPlatformWeb::_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) {
void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, BitField<EditorExportPlatform::DebugFlags> 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));
Vector<String> flags = gen_export_flags(p_flags & (~DEBUG_FLAG_DUMB_CLIENT));
Array args;
for (int i = 0; i < flags.size(); i++) {
args.push_back(flags[i]);
@ -170,6 +167,13 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito
replaces["$GODOT_PROJECT_NAME"] = GLOBAL_GET("application/config/name");
replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;
replaces["$GODOT_CONFIG"] = str_config;
replaces["$GODOT_SPLASH_COLOR"] = "#" + Color(GLOBAL_GET("application/boot_splash/bg_color")).to_html(false);
LocalVector<String> godot_splash_classes;
godot_splash_classes.push_back("show-image--" + String(GLOBAL_GET("application/boot_splash/show_image")));
godot_splash_classes.push_back("fullsize--" + String(GLOBAL_GET("application/boot_splash/fullsize")));
godot_splash_classes.push_back("use-filter--" + String(GLOBAL_GET("application/boot_splash/use_filter")));
replaces["$GODOT_SPLASH_CLASSES"] = String(" ").join(godot_splash_classes);
replaces["$GODOT_SPLASH"] = p_name + ".png";
if (p_preset->get("variant/thread_support")) {
@ -188,9 +192,9 @@ Error EditorExportPlatformWeb::_add_manifest_icon(const String &p_path, const St
Ref<Image> icon;
if (!p_icon.is_empty()) {
icon.instantiate();
const Error err = ImageLoader::load_image(p_icon, icon);
if (err != OK) {
Error err = OK;
icon = _load_icon_or_splash_image(p_icon, &err);
if (err != OK || icon.is_null() || icon->is_empty()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Icon Creation"), vformat(TTR("Could not read file: \"%s\"."), p_icon));
return err;
}
@ -240,8 +244,9 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
cache_files.push_back(name + ".icon.png");
cache_files.push_back(name + ".apple-touch-icon.png");
}
cache_files.push_back(name + ".worker.js");
cache_files.push_back(name + ".audio.worklet.js");
cache_files.push_back(name + ".audio.position.worklet.js");
replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string();
// Heavy files that are cached on demand.
@ -333,9 +338,11 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
void EditorExportPlatformWeb::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const {
if (p_preset->get("vram_texture_compression/for_desktop")) {
r_features->push_back("s3tc");
r_features->push_back("bptc");
}
if (p_preset->get("vram_texture_compression/for_mobile")) {
r_features->push_back("etc2");
r_features->push_back("astc");
}
if (p_preset->get("variant/thread_support").operator bool()) {
r_features->push_back("threads");
@ -371,6 +378,16 @@ void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "progressive_web_app/background_color", PROPERTY_HINT_COLOR_NO_ALPHA), Color()));
}
bool EditorExportPlatformWeb::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const {
bool advanced_options_enabled = p_preset->are_advanced_options_enabled();
if (p_option == "custom_template/debug" ||
p_option == "custom_template/release") {
return advanced_options_enabled;
}
return true;
}
String EditorExportPlatformWeb::get_name() const {
return "Web";
}
@ -449,7 +466,7 @@ List<String> EditorExportPlatformWeb::get_binary_extensions(const Ref<EditorExpo
return list;
}
Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) {
ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags);
const String custom_debug = p_preset->get("custom_template/debug");
@ -743,7 +760,7 @@ String EditorExportPlatformWeb::get_option_tooltip(int p_index) const {
return "";
}
Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) {
Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) {
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");
@ -816,7 +833,7 @@ Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int
}
Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags) {
const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("web");
const String dest = EditorPaths::get_singleton()->get_temp_dir().path_join("web");
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
if (!da->dir_exists(dest)) {
Error err = da->make_dir_recursive(dest);
@ -833,8 +850,8 @@ Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_
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 + ".audio.position.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");
@ -887,7 +904,6 @@ EditorExportPlatformWeb::EditorExportPlatformWeb() {
if (EditorNode::get_singleton()) {
server.instantiate();
#ifdef MODULE_SVG_ENABLED
Ref<Image> img = memnew(Image);
const bool upsample = !Math::is_equal_approx(Math::round(EDSCALE), EDSCALE);
@ -896,7 +912,6 @@ EditorExportPlatformWeb::EditorExportPlatformWeb() {
ImageLoaderSVG::create_image_from_string(img, _web_run_icon_svg, EDSCALE, upsample, false);
run_icon = ImageTexture::create_from_image(img);
#endif
Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
if (theme.is_valid()) {

View file

@ -77,20 +77,28 @@ class EditorExportPlatformWeb : public EditorExportPlatform {
}
Ref<Image> _get_project_icon() const {
Error err = OK;
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) {
if (!icon_path.is_empty()) {
icon = _load_icon_or_splash_image(icon_path, &err);
}
if (icon_path.is_empty() || err != OK || icon.is_null() || icon->is_empty()) {
return EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("DefaultProjectIcon"), EditorStringName(EditorIcons))->get_image();
}
return icon;
}
Ref<Image> _get_project_splash() const {
Error err = OK;
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) {
if (!splash_path.is_empty()) {
splash = _load_icon_or_splash_image(splash_path, &err);
}
if (splash_path.is_empty() || err != OK || splash.is_null() || splash->is_empty()) {
return Ref<Image>(memnew(Image(boot_splash_png)));
}
return splash;
@ -98,7 +106,7 @@ class EditorExportPlatformWeb : public EditorExportPlatform {
Error _extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa);
void _replace_strings(const HashMap<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);
void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, BitField<EditorExportPlatform::DebugFlags> 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);
@ -112,6 +120,7 @@ public:
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
virtual void get_export_options(List<ExportOption> *r_options) const override;
virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override;
virtual String get_name() const override;
virtual String get_os_name() const override;
@ -120,14 +129,14 @@ public:
virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override;
virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) 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 Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> 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;
virtual String get_option_tooltip(int p_index) const override;
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 Error run(const Ref<EditorExportPreset> &p_preset, int p_option, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) override;
virtual Ref<Texture2D> get_run_icon() const override;
virtual void get_platform_features(List<String> *r_features) const override {

View file

@ -51,10 +51,11 @@ extern void godot_audio_input_stop();
extern int godot_audio_sample_stream_is_registered(const char *p_stream_object_id);
extern void godot_audio_sample_register_stream(const char *p_stream_object_id, float *p_frames_buf, int p_frames_total, const char *p_loop_mode, int p_loop_begin, int p_loop_end);
extern void godot_audio_sample_unregister_stream(const char *p_stream_object_id);
extern void godot_audio_sample_start(const char *p_playback_object_id, const char *p_stream_object_id, int p_bus_index, float p_offset, float *p_volume_ptr);
extern void godot_audio_sample_start(const char *p_playback_object_id, const char *p_stream_object_id, int p_bus_index, float p_offset, float p_pitch_scale, float *p_volume_ptr);
extern void godot_audio_sample_stop(const char *p_playback_object_id);
extern void godot_audio_sample_set_pause(const char *p_playback_object_id, bool p_pause);
extern int godot_audio_sample_is_active(const char *p_playback_object_id);
extern double godot_audio_get_sample_playback_position(const char *p_playback_object_id);
extern void godot_audio_sample_update_pitch_scale(const char *p_playback_object_id, float p_pitch_scale);
extern void godot_audio_sample_set_volumes_linear(const char *p_playback_object_id, int *p_buses_buf, int p_buses_size, float *p_volumes_buf, int p_volumes_size);
extern void godot_audio_sample_set_finished_callback(void (*p_callback)(const char *));

View file

@ -0,0 +1,51 @@
/**************************************************************************/
/* godot_midi.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef GODOT_MIDI_H
#define GODOT_MIDI_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
extern int godot_js_webmidi_open_midi_inputs(
void (*p_callback)(int p_size, const char **p_connected_input_names),
void (*p_on_midi_message)(int p_device_index, int p_status, const uint8_t *p_data, int p_data_len),
const uint8_t *p_data_buffer,
const int p_data_buffer_len);
extern void godot_js_webmidi_close_midi_inputs();
#ifdef __cplusplus
}
#endif
#endif // GODOT_MIDI_H

View file

@ -266,11 +266,11 @@ Error HTTPClientWeb::poll() {
return OK;
}
HTTPClient *HTTPClientWeb::_create_func() {
return memnew(HTTPClientWeb);
HTTPClient *HTTPClientWeb::_create_func(bool p_notify_postinitialize) {
return static_cast<HTTPClient *>(ClassDB::creator<HTTPClientWeb>(p_notify_postinitialize));
}
HTTPClient *(*HTTPClient::_create)() = HTTPClientWeb::_create_func;
HTTPClient *(*HTTPClient::_create)(bool p_notify_postinitialize) = HTTPClientWeb::_create_func;
HTTPClientWeb::HTTPClientWeb() {
}

View file

@ -81,7 +81,7 @@ private:
static void _parse_headers(int p_len, const char **p_headers, void *p_ref);
public:
static HTTPClient *_create_func();
static HTTPClient *_create_func(bool p_notify_postinitialize);
Error request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_size) override;

View file

@ -0,0 +1,48 @@
/**************************************************************************/
/* ip_web.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 "ip_web.h"
void IPWeb::_resolve_hostname(List<IPAddress> &r_addresses, const String &p_hostname, Type p_type) const {
}
void IPWeb::get_local_interfaces(HashMap<String, Interface_Info> *r_interfaces) const {
}
void IPWeb::make_default() {
_create = _create_web;
}
IP *IPWeb::_create_web() {
return memnew(IPWeb);
}
IPWeb::IPWeb() {
}

View file

@ -0,0 +1,51 @@
/**************************************************************************/
/* ip_web.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 IP_WEB_H
#define IP_WEB_H
#include "core/io/ip.h"
class IPWeb : public IP {
GDCLASS(IPWeb, IP);
virtual void _resolve_hostname(List<IPAddress> &r_addresses, const String &p_hostname, Type p_type = TYPE_ANY) const override;
private:
static IP *_create_web();
public:
virtual void get_local_interfaces(HashMap<String, Interface_Info> *r_interfaces) const override;
static void make_default();
IPWeb();
};
#endif // IP_WEB_H

View file

@ -59,6 +59,8 @@ extern void godot_js_wrapper_object_unref(int p_id);
extern int godot_js_wrapper_create_cb(void *p_ref, void (*p_callback)(void *p_ref, int p_arg_id, int p_argc));
extern void godot_js_wrapper_object_set_cb_ret(int p_type, godot_js_wrapper_ex *p_val);
extern int godot_js_wrapper_create_object(const char *p_method, void **p_args, int p_argc, GodotJSWrapperVariant2JSCallback p_variant2js_callback, godot_js_wrapper_ex *p_cb_rval, void **p_lock, GodotJSWrapperFreeLockCallback p_lock_callback);
extern int godot_js_wrapper_object_is_buffer(int p_id);
extern int godot_js_wrapper_object_transfer_buffer(int p_id, void *p_byte_arr, void *p_byte_arr_write, void *(*p_callback)(void *p_ptr, void *p_ptr2, int p_len));
};
class JavaScriptObjectImpl : public JavaScriptObject {
@ -304,7 +306,7 @@ Variant JavaScriptBridge::_create_object_bind(const Variant **p_args, int p_argc
r_error.expected = 1;
return Ref<JavaScriptObject>();
}
if (p_args[0]->get_type() != Variant::STRING) {
if (!p_args[0]->is_string()) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
r_error.argument = 0;
r_error.expected = Variant::STRING;
@ -366,6 +368,27 @@ Variant JavaScriptBridge::eval(const String &p_code, bool p_use_global_exec_cont
}
}
bool JavaScriptBridge::is_js_buffer(Ref<JavaScriptObject> p_js_obj) {
Ref<JavaScriptObjectImpl> obj = p_js_obj;
if (obj.is_null()) {
return false;
}
return godot_js_wrapper_object_is_buffer(obj->_js_id);
}
PackedByteArray JavaScriptBridge::js_buffer_to_packed_byte_array(Ref<JavaScriptObject> p_js_obj) {
ERR_FAIL_COND_V_MSG(!is_js_buffer(p_js_obj), PackedByteArray(), "The JavaScript object is not a buffer.");
Ref<JavaScriptObjectImpl> obj = p_js_obj;
PackedByteArray arr;
VectorWriteProxy<uint8_t> arr_write;
godot_js_wrapper_object_transfer_buffer(obj->_js_id, &arr, &arr_write, resize_PackedByteArray_and_open_write);
arr_write = VectorWriteProxy<uint8_t>();
return arr;
}
#endif // JAVASCRIPT_EVAL_ENABLED
void JavaScriptBridge::download_buffer(Vector<uint8_t> p_arr, const String &p_name, const String &p_mime) {

View file

@ -295,10 +295,10 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
'locateFile': function (path) {
if (!path.startsWith('godot.')) {
return path;
} else if (path.endsWith('.worker.js')) {
return `${loadPath}.worker.js`;
} else if (path.endsWith('.audio.worklet.js')) {
return `${loadPath}.audio.worklet.js`;
} else if (path.endsWith('.audio.position.worklet.js')) {
return `${loadPath}.audio.position.worklet.js`;
} else if (path.endsWith('.js')) {
return `${loadPath}.js`;
} else if (path in gdext) {

View file

@ -241,7 +241,11 @@ const Engine = (function () {
*/
installServiceWorker: function () {
if (this.config.serviceWorker && 'serviceWorker' in navigator) {
return navigator.serviceWorker.register(this.config.serviceWorker);
try {
return navigator.serviceWorker.register(this.config.serviceWorker);
} catch (e) {
return Promise.reject(e);
}
}
return Promise.resolve();
},

View file

@ -0,0 +1,69 @@
/**************************************************************************/
/* godot.audio.position.worklet.js */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
const POST_THRESHOLD_S = 0.1;
class GodotPositionReportingProcessor extends AudioWorkletProcessor {
constructor(...args) {
super(...args);
this.lastPostTime = currentTime;
this.position = 0;
this.ended = false;
this.port.onmessage = (event) => {
if (event?.data?.type === 'ended') {
this.ended = true;
}
};
}
process(inputs, _outputs, _parameters) {
if (this.ended) {
return false;
}
if (inputs.length > 0) {
const input = inputs[0];
if (input.length > 0) {
this.position += input[0].length;
}
}
// Posting messages is expensive. Let's limit the number of posts.
if (currentTime - this.lastPostTime > POST_THRESHOLD_S) {
this.lastPostTime = currentTime;
this.port.postMessage({ type: 'position', data: this.position });
}
return true;
}
}
registerProcessor('godot-position-reporting-processor', GodotPositionReportingProcessor);

View file

@ -77,7 +77,7 @@ class Sample {
* Creates a `Sample` based on the params. Will register it to the
* `GodotAudio.samples` registry.
* @param {SampleParams} params Base params
* @param {SampleOptions} [options={{}}] Optional params
* @param {SampleOptions | undefined} options Optional params.
* @returns {Sample}
*/
static create(params, options = {}) {
@ -98,7 +98,7 @@ class Sample {
/**
* `Sample` constructor.
* @param {SampleParams} params Base params
* @param {SampleOptions} [options={{}}] Optional params
* @param {SampleOptions | undefined} options Optional params.
*/
constructor(params, options = {}) {
/** @type {string} */
@ -328,8 +328,10 @@ class SampleNodeBus {
* offset?: number
* playbackRate?: number
* startTime?: number
* pitchScale?: number
* loopMode?: LoopMode
* volume?: Float32Array
* start?: boolean
* }} SampleNodeOptions
*/
@ -391,7 +393,7 @@ class SampleNode {
* Creates a `SampleNode` based on the params. Will register the `SampleNode` to
* the `GodotAudio.sampleNodes` regisery.
* @param {SampleNodeParams} params Base params.
* @param {SampleNodeOptions} options Optional params.
* @param {SampleNodeOptions | undefined} options Optional params.
* @returns {SampleNode}
*/
static create(params, options = {}) {
@ -411,7 +413,7 @@ class SampleNode {
/**
* @param {SampleNodeParams} params Base params
* @param {SampleNodeOptions} [options={{}}] Optional params
* @param {SampleNodeOptions | undefined} options Optional params.
*/
constructor(params, options = {}) {
/** @type {string} */
@ -421,9 +423,15 @@ class SampleNode {
/** @type {number} */
this.offset = options.offset ?? 0;
/** @type {number} */
this._playbackPosition = options.offset;
/** @type {number} */
this.startTime = options.startTime ?? 0;
/** @type {boolean} */
this.isPaused = false;
/** @type {boolean} */
this.isStarted = false;
/** @type {boolean} */
this.isCanceled = false;
/** @type {number} */
this.pauseTime = 0;
/** @type {number} */
@ -431,7 +439,7 @@ class SampleNode {
/** @type {LoopMode} */
this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled';
/** @type {number} */
this._pitchScale = 1;
this._pitchScale = options.pitchScale ?? 1;
/** @type {number} */
this._sourceStartTime = 0;
/** @type {Map<Bus, SampleNodeBus>} */
@ -440,6 +448,8 @@ class SampleNode {
this._source = GodotAudio.ctx.createBufferSource();
this._onended = null;
/** @type {AudioWorkletNode | null} */
this._positionWorklet = null;
this.setPlaybackRate(options.playbackRate ?? 44100);
this._source.buffer = this.getSample().getAudioBuffer();
@ -449,6 +459,12 @@ class SampleNode {
const bus = GodotAudio.Bus.getBus(params.busIndex);
const sampleNodeBus = this.getSampleNodeBus(bus);
sampleNodeBus.setVolume(options.volume);
this.connectPositionWorklet(options.start).catch((err) => {
const newErr = new Error('Failed to create PositionWorklet.');
newErr.cause = err;
GodotRuntime.error(newErr);
});
}
/**
@ -459,6 +475,14 @@ class SampleNode {
return this._playbackRate;
}
/**
* Gets the playback position.
* @returns {number}
*/
getPlaybackPosition() {
return this._playbackPosition;
}
/**
* Sets the playback rate.
* @param {number} val Value to set.
@ -508,8 +532,12 @@ class SampleNode {
* @returns {void}
*/
start() {
if (this.isStarted) {
return;
}
this._resetSourceStartTime();
this._source.start(this.startTime, this.offset);
this.isStarted = true;
}
/**
@ -584,18 +612,60 @@ class SampleNode {
return this._sampleNodeBuses.get(bus);
}
/**
* Sets up and connects the source to the GodotPositionReportingProcessor
* If the worklet module is not loaded in, it will be added
*/
async connectPositionWorklet(start) {
await GodotAudio.audioPositionWorkletPromise;
if (this.isCanceled) {
return;
}
this._source.connect(this.getPositionWorklet());
if (start) {
this.start();
}
}
/**
* Get a AudioWorkletProcessor
* @returns {AudioWorkletNode}
*/
getPositionWorklet() {
if (this._positionWorklet != null) {
return this._positionWorklet;
}
this._positionWorklet = new AudioWorkletNode(
GodotAudio.ctx,
'godot-position-reporting-processor'
);
this._positionWorklet.port.onmessage = (event) => {
switch (event.data['type']) {
case 'position':
this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset;
break;
default:
// Do nothing.
}
};
return this._positionWorklet;
}
/**
* Clears the `SampleNode`.
* @returns {void}
*/
clear() {
this.isCanceled = true;
this.isPaused = false;
this.pauseTime = 0;
if (this._source != null) {
this._source.removeEventListener('ended', this._onended);
this._onended = null;
this._source.stop();
if (this.isStarted) {
this._source.stop();
}
this._source.disconnect();
this._source = null;
}
@ -605,6 +675,13 @@ class SampleNode {
}
this._sampleNodeBuses.clear();
if (this._positionWorklet) {
this._positionWorklet.disconnect();
this._positionWorklet.port.onmessage = null;
this._positionWorklet.port.postMessage({ type: 'ended' });
this._positionWorklet = null;
}
GodotAudio.SampleNode.delete(this.id);
}
@ -645,7 +722,12 @@ class SampleNode {
const pauseTime = this.isPaused
? this.pauseTime
: 0;
if (this._positionWorklet != null) {
this._positionWorklet.port.postMessage({ type: 'clear' });
this._source.connect(this._positionWorklet);
}
this._source.start(this.startTime, this.offset + pauseTime);
this.isStarted = true;
}
/**
@ -653,6 +735,9 @@ class SampleNode {
* @returns {void}
*/
_pause() {
if (!this.isStarted) {
return;
}
this.isPaused = true;
this.pauseTime = (GodotAudio.ctx.currentTime - this._sourceStartTime) / this.getPlaybackRate();
this._source.stop();
@ -780,7 +865,10 @@ class Bus {
* @returns {void}
*/
static move(fromIndex, toIndex) {
const movedBus = GodotAudio.Bus.getBus(fromIndex);
const movedBus = GodotAudio.Bus.getBusOrNull(fromIndex);
if (movedBus == null) {
return;
}
const buses = GodotAudio.buses.filter((_, i) => i !== fromIndex);
// Inserts at index.
buses.splice(toIndex - 1, 0, movedBus);
@ -1108,6 +1196,9 @@ const _GodotAudio = {
driver: null,
interval: 0,
/** @type {Promise} */
audioPositionWorkletPromise: null,
/**
* Converts linear volume to Db.
* @param {number} linear Linear value to convert.
@ -1174,6 +1265,10 @@ const _GodotAudio = {
onlatencyupdate(computed_latency);
}, 1000);
GodotOS.atexit(GodotAudio.close_async);
const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
GodotAudio.audioPositionWorkletPromise = ctx.audioWorklet.addModule(path);
return ctx.destination.channelCount;
},
@ -1252,7 +1347,7 @@ const _GodotAudio = {
* @param {string} playbackObjectId The unique id of the sample playback
* @param {string} streamObjectId The unique id of the stream
* @param {number} busIndex Index of the bus currently binded to the sample playback
* @param {SampleNodeOptions} startOptions Optional params
* @param {SampleNodeOptions | undefined} startOptions Optional params.
* @returns {void}
*/
start_sample: function (
@ -1262,7 +1357,7 @@ const _GodotAudio = {
startOptions
) {
GodotAudio.SampleNode.stopSampleNode(playbackObjectId);
const sampleNode = GodotAudio.SampleNode.create(
GodotAudio.SampleNode.create(
{
busIndex,
id: playbackObjectId,
@ -1270,7 +1365,6 @@ const _GodotAudio = {
},
startOptions
);
sampleNode.start();
},
/**
@ -1337,7 +1431,10 @@ const _GodotAudio = {
* @returns {void}
*/
remove_sample_bus: function (index) {
const bus = GodotAudio.Bus.getBus(index);
const bus = GodotAudio.Bus.getBusOrNull(index);
if (bus == null) {
return;
}
bus.clear();
},
@ -1367,8 +1464,17 @@ const _GodotAudio = {
* @returns {void}
*/
set_sample_bus_send: function (busIndex, sendIndex) {
const bus = GodotAudio.Bus.getBus(busIndex);
bus.setSend(GodotAudio.Bus.getBus(sendIndex));
const bus = GodotAudio.Bus.getBusOrNull(busIndex);
if (bus == null) {
// Cannot send from an invalid bus.
return;
}
let targetBus = GodotAudio.Bus.getBusOrNull(sendIndex);
if (targetBus == null) {
// Send to master.
targetBus = GodotAudio.Bus.getBus(0);
}
bus.setSend(targetBus);
},
/**
@ -1378,7 +1484,10 @@ const _GodotAudio = {
* @returns {void}
*/
set_sample_bus_volume_db: function (busIndex, volumeDb) {
const bus = GodotAudio.Bus.getBus(busIndex);
const bus = GodotAudio.Bus.getBusOrNull(busIndex);
if (bus == null) {
return;
}
bus.setVolumeDb(volumeDb);
},
@ -1389,7 +1498,10 @@ const _GodotAudio = {
* @returns {void}
*/
set_sample_bus_solo: function (busIndex, enable) {
const bus = GodotAudio.Bus.getBus(busIndex);
const bus = GodotAudio.Bus.getBusOrNull(busIndex);
if (bus == null) {
return;
}
bus.solo(enable);
},
@ -1400,7 +1512,10 @@ const _GodotAudio = {
* @returns {void}
*/
set_sample_bus_mute: function (busIndex, enable) {
const bus = GodotAudio.Bus.getBus(busIndex);
const bus = GodotAudio.Bus.getBusOrNull(busIndex);
if (bus == null) {
return;
}
bus.mute(enable);
},
},
@ -1562,13 +1677,14 @@ const _GodotAudio = {
},
godot_audio_sample_start__proxy: 'sync',
godot_audio_sample_start__sig: 'viiiii',
godot_audio_sample_start__sig: 'viiiifi',
/**
* Starts a sample.
* @param {number} playbackObjectIdStrPtr Playback object id pointer
* @param {number} streamObjectIdStrPtr Stream object id pointer
* @param {number} busIndex Bus index
* @param {number} offset Sample offset
* @param {number} pitchScale Pitch scale
* @param {number} volumePtr Volume pointer
* @returns {void}
*/
@ -1577,6 +1693,7 @@ const _GodotAudio = {
streamObjectIdStrPtr,
busIndex,
offset,
pitchScale,
volumePtr
) {
/** @type {string} */
@ -1585,11 +1702,13 @@ const _GodotAudio = {
const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr);
/** @type {Float32Array} */
const volume = GodotRuntime.heapSub(HEAPF32, volumePtr, 8);
/** @type {SampleNodeConstructorOptions} */
/** @type {SampleNodeOptions} */
const startOptions = {
offset,
volume,
playbackRate: 1,
pitchScale,
start: true,
};
GodotAudio.start_sample(
playbackObjectId,
@ -1635,6 +1754,22 @@ const _GodotAudio = {
return Number(GodotAudio.sampleNodes.has(playbackObjectId));
},
godot_audio_get_sample_playback_position__proxy: 'sync',
godot_audio_get_sample_playback_position__sig: 'di',
/**
* Returns the position of the playback position.
* @param {number} playbackObjectIdStrPtr Playback object id pointer
* @returns {number}
*/
godot_audio_get_sample_playback_position: function (playbackObjectIdStrPtr) {
const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);
const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId);
if (sampleNode == null) {
return 0;
}
return sampleNode.getPlaybackPosition();
},
godot_audio_sample_update_pitch_scale__proxy: 'sync',
godot_audio_sample_update_pitch_scale__sig: 'vii',
/**
@ -2029,7 +2164,7 @@ autoAddDeps(GodotAudioWorklet, '$GodotAudioWorklet');
mergeInto(LibraryManager.library, GodotAudioWorklet);
/*
* The ScriptProcessorNode API, used when threads are disabled.
* The ScriptProcessorNode API, used as a fallback if AudioWorklet is not available.
*/
const GodotAudioScript = {
$GodotAudioScript__deps: ['$GodotAudio'],

View file

@ -59,7 +59,12 @@ const GodotFetch = {
});
obj.status = response.status;
obj.response = response;
obj.reader = response.body.getReader();
// `body` can be null per spec (for example, in cases where the request method is HEAD).
// As of the time of writing, Chromium (127.0.6533.72) does not follow the spec but Firefox (131.0.3) does.
// See godotengine/godot#76825 for more information.
// See Chromium revert (of the change to follow the spec):
// https://chromium.googlesource.com/chromium/src/+/135354b7bdb554cd03c913af7c90aceead03c4d4
obj.reader = response.body?.getReader();
obj.chunked = chunked;
},
@ -121,6 +126,10 @@ const GodotFetch = {
}
obj.reading = true;
obj.reader.read().then(GodotFetch.onread.bind(null, id)).catch(GodotFetch.onerror.bind(null, id));
} else if (obj.reader == null && obj.response.body == null) {
// Emulate a stream closure to maintain the request lifecycle.
obj.reading = true;
GodotFetch.onread(id, { value: undefined, done: true });
}
},
},
@ -159,7 +168,10 @@ const GodotFetch = {
if (!obj.response) {
return 0;
}
if (obj.reader) {
// If the reader is nullish, but there is no body, and the request is not marked as done,
// the same status should be returned as though the request is currently being read
// so that the proper lifecycle closure can be handled in `read()`.
if (obj.reader || (obj.response.body == null && !obj.done)) {
return 1;
}
if (obj.done) {

View file

@ -38,41 +38,57 @@ const GodotIME = {
$GodotIME: {
ime: null,
active: false,
focusTimerIntervalId: -1,
getModifiers: function (evt) {
return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3);
},
ime_active: function (active) {
function focus_timer() {
GodotIME.active = true;
function clearFocusTimerInterval() {
clearInterval(GodotIME.focusTimerIntervalId);
GodotIME.focusTimerIntervalId = -1;
}
function focusTimer() {
if (GodotIME.ime == null) {
clearFocusTimerInterval();
return;
}
GodotIME.ime.focus();
}
if (GodotIME.ime) {
if (active) {
GodotIME.ime.style.display = 'block';
setInterval(focus_timer, 100);
} else {
GodotIME.ime.style.display = 'none';
GodotConfig.canvas.focus();
GodotIME.active = false;
}
if (GodotIME.focusTimerIntervalId > -1) {
clearFocusTimerInterval();
}
if (GodotIME.ime == null) {
return;
}
GodotIME.active = active;
if (active) {
GodotIME.ime.style.display = 'block';
GodotIME.focusTimerIntervalId = setInterval(focusTimer, 100);
} else {
GodotIME.ime.style.display = 'none';
GodotConfig.canvas.focus();
}
},
ime_position: function (x, y) {
if (GodotIME.ime) {
const canvas = GodotConfig.canvas;
const rect = canvas.getBoundingClientRect();
const rw = canvas.width / rect.width;
const rh = canvas.height / rect.height;
const clx = (x / rw) + rect.x;
const cly = (y / rh) + rect.y;
GodotIME.ime.style.left = `${clx}px`;
GodotIME.ime.style.top = `${cly}px`;
if (GodotIME.ime == null) {
return;
}
const canvas = GodotConfig.canvas;
const rect = canvas.getBoundingClientRect();
const rw = canvas.width / rect.width;
const rh = canvas.height / rect.height;
const clx = (x / rw) + rect.x;
const cly = (y / rh) + rect.y;
GodotIME.ime.style.left = `${clx}px`;
GodotIME.ime.style.top = `${cly}px`;
},
init: function (ime_cb, key_cb, code, key) {
@ -84,20 +100,27 @@ const GodotIME = {
evt.preventDefault();
}
function ime_event_cb(event) {
if (GodotIME.ime) {
if (event.type === 'compositionstart') {
ime_cb(0, null);
GodotIME.ime.innerHTML = '';
} else if (event.type === 'compositionupdate') {
const ptr = GodotRuntime.allocString(event.data);
ime_cb(1, ptr);
GodotRuntime.free(ptr);
} else if (event.type === 'compositionend') {
const ptr = GodotRuntime.allocString(event.data);
ime_cb(2, ptr);
GodotRuntime.free(ptr);
GodotIME.ime.innerHTML = '';
}
if (GodotIME.ime == null) {
return;
}
switch (event.type) {
case 'compositionstart':
ime_cb(0, null);
GodotIME.ime.innerHTML = '';
break;
case 'compositionupdate': {
const ptr = GodotRuntime.allocString(event.data);
ime_cb(1, ptr);
GodotRuntime.free(ptr);
} break;
case 'compositionend': {
const ptr = GodotRuntime.allocString(event.data);
ime_cb(2, ptr);
GodotRuntime.free(ptr);
GodotIME.ime.innerHTML = '';
} break;
default:
// Do nothing.
}
}
@ -133,10 +156,15 @@ const GodotIME = {
},
clear: function () {
if (GodotIME.ime) {
GodotIME.ime.remove();
GodotIME.ime = null;
if (GodotIME.ime == null) {
return;
}
if (GodotIME.focusTimerIntervalId > -1) {
clearInterval(GodotIME.focusTimerIntervalId);
GodotIME.focusTimerIntervalId = -1;
}
GodotIME.ime.remove();
GodotIME.ime = null;
},
},
};

View file

@ -127,6 +127,10 @@ const GodotJSWrapper = {
GodotRuntime.setHeapValue(p_exchange, id, 'i64');
return 24; // OBJECT
},
isBuffer: function (obj) {
return obj instanceof ArrayBuffer || ArrayBuffer.isView(obj);
},
},
godot_js_wrapper_interface_get__proxy: 'sync',
@ -303,6 +307,34 @@ const GodotJSWrapper = {
return -1;
}
},
godot_js_wrapper_object_is_buffer__proxy: 'sync',
godot_js_wrapper_object_is_buffer__sig: 'ii',
godot_js_wrapper_object_is_buffer: function (p_id) {
const obj = GodotJSWrapper.get_proxied_value(p_id);
return GodotJSWrapper.isBuffer(obj)
? 1
: 0;
},
godot_js_wrapper_object_transfer_buffer__proxy: 'sync',
godot_js_wrapper_object_transfer_buffer__sig: 'viiii',
godot_js_wrapper_object_transfer_buffer: function (p_id, p_byte_arr, p_byte_arr_write, p_callback) {
let obj = GodotJSWrapper.get_proxied_value(p_id);
if (!GodotJSWrapper.isBuffer(obj)) {
return;
}
if (ArrayBuffer.isView(obj) && !(obj instanceof Uint8Array)) {
obj = new Uint8Array(obj.buffer);
} else if (obj instanceof ArrayBuffer) {
obj = new Uint8Array(obj);
}
const resizePackedByteArrayAndOpenWrite = GodotRuntime.get_func(p_callback);
const bytesPtr = resizePackedByteArrayAndOpenWrite(p_byte_arr, p_byte_arr_write, obj.length);
HEAPU8.set(obj, bytesPtr);
},
};
autoAddDeps(GodotJSWrapper, '$GodotJSWrapper');

View file

@ -441,8 +441,12 @@ const GodotPWA = {
godot_js_pwa_cb__sig: 'vi',
godot_js_pwa_cb: function (p_update_cb) {
if ('serviceWorker' in navigator) {
const cb = GodotRuntime.get_func(p_update_cb);
navigator.serviceWorker.getRegistration().then(GodotPWA.updateState.bind(null, cb));
try {
const cb = GodotRuntime.get_func(p_update_cb);
navigator.serviceWorker.getRegistration().then(GodotPWA.updateState.bind(null, cb));
} catch (e) {
GodotRuntime.error('Failed to assign PWA callback', e);
}
}
},
@ -450,12 +454,17 @@ const GodotPWA = {
godot_js_pwa_update__sig: 'i',
godot_js_pwa_update: function () {
if ('serviceWorker' in navigator && GodotPWA.hasUpdate) {
navigator.serviceWorker.getRegistration().then(function (reg) {
if (!reg || !reg.waiting) {
return;
}
reg.waiting.postMessage('update');
});
try {
navigator.serviceWorker.getRegistration().then(function (reg) {
if (!reg || !reg.waiting) {
return;
}
reg.waiting.postMessage('update');
});
} catch (e) {
GodotRuntime.error(e);
return 1;
}
return 0;
}
return 1;

View file

@ -0,0 +1,94 @@
/**************************************************************************/
/* library_godot_webmidi.js */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
const GodotWebMidi = {
$GodotWebMidi__deps: ['$GodotRuntime'],
$GodotWebMidi: {
abortControllers: [],
isListening: false,
},
godot_js_webmidi_open_midi_inputs__deps: ['$GodotWebMidi'],
godot_js_webmidi_open_midi_inputs__proxy: 'sync',
godot_js_webmidi_open_midi_inputs__sig: 'iiii',
godot_js_webmidi_open_midi_inputs: function (pSetInputNamesCb, pOnMidiMessageCb, pDataBuffer, dataBufferLen) {
if (GodotWebMidi.is_listening) {
return 0; // OK
}
if (!navigator.requestMIDIAccess) {
return 2; // ERR_UNAVAILABLE
}
const setInputNamesCb = GodotRuntime.get_func(pSetInputNamesCb);
const onMidiMessageCb = GodotRuntime.get_func(pOnMidiMessageCb);
GodotWebMidi.isListening = true;
navigator.requestMIDIAccess().then((midi) => {
const inputs = [...midi.inputs.values()];
const inputNames = inputs.map((input) => input.name);
const c_ptr = GodotRuntime.allocStringArray(inputNames);
setInputNamesCb(inputNames.length, c_ptr);
GodotRuntime.freeStringArray(c_ptr, inputNames.length);
inputs.forEach((input, i) => {
const abortController = new AbortController();
GodotWebMidi.abortControllers.push(abortController);
input.addEventListener('midimessage', (event) => {
const status = event.data[0];
const data = event.data.slice(1);
const size = data.length;
if (size > dataBufferLen) {
throw new Error(`data too big ${size} > ${dataBufferLen}`);
}
HEAPU8.set(data, pDataBuffer);
onMidiMessageCb(i, status, pDataBuffer, data.length);
}, { signal: abortController.signal });
});
});
return 0; // OK
},
godot_js_webmidi_close_midi_inputs__deps: ['$GodotWebMidi'],
godot_js_webmidi_close_midi_inputs__proxy: 'sync',
godot_js_webmidi_close_midi_inputs__sig: 'v',
godot_js_webmidi_close_midi_inputs: function () {
for (const abortController of GodotWebMidi.abortControllers) {
abortController.abort();
}
GodotWebMidi.abortControllers = [];
GodotWebMidi.isListening = false;
},
};
mergeInto(LibraryManager.library, GodotWebMidi);

View file

@ -0,0 +1,39 @@
/**************************************************************************/
/* net_socket_web.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 "net_socket_web.h"
NetSocket *NetSocketWeb::_create_func() {
return memnew(NetSocketWeb);
}
void NetSocketWeb::make_default() {
_create = _create_func;
}

View file

@ -0,0 +1,72 @@
/**************************************************************************/
/* net_socket_web.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 NET_SOCKET_WEB_H
#define NET_SOCKET_WEB_H
#include "core/io/net_socket.h"
#include <sys/socket.h>
class NetSocketWeb : public NetSocket {
protected:
static NetSocket *_create_func();
public:
static void make_default();
virtual Error open(Type p_sock_type, IP::Type &ip_type) override { return ERR_UNAVAILABLE; }
virtual void close() override {}
virtual Error bind(IPAddress p_addr, uint16_t p_port) override { return ERR_UNAVAILABLE; }
virtual Error listen(int p_max_pending) override { return ERR_UNAVAILABLE; }
virtual Error connect_to_host(IPAddress p_host, uint16_t p_port) override { return ERR_UNAVAILABLE; }
virtual Error poll(PollType p_type, int timeout) const override { return ERR_UNAVAILABLE; }
virtual Error recv(uint8_t *p_buffer, int p_len, int &r_read) override { return ERR_UNAVAILABLE; }
virtual Error recvfrom(uint8_t *p_buffer, int p_len, int &r_read, IPAddress &r_ip, uint16_t &r_port, bool p_peek = false) override { return ERR_UNAVAILABLE; }
virtual Error send(const uint8_t *p_buffer, int p_len, int &r_sent) override { return ERR_UNAVAILABLE; }
virtual Error sendto(const uint8_t *p_buffer, int p_len, int &r_sent, IPAddress p_ip, uint16_t p_port) override { return ERR_UNAVAILABLE; }
virtual Ref<NetSocket> accept(IPAddress &r_ip, uint16_t &r_port) override { return Ref<NetSocket>(); }
virtual bool is_open() const override { return false; }
virtual int get_available_bytes() const override { return -1; }
virtual Error get_socket_address(IPAddress *r_ip, uint16_t *r_port) const override { return ERR_UNAVAILABLE; }
virtual Error set_broadcasting_enabled(bool p_enabled) override { return ERR_UNAVAILABLE; }
virtual void set_blocking_enabled(bool p_enabled) override {}
virtual void set_ipv6_only_enabled(bool p_enabled) override {}
virtual void set_tcp_no_delay_enabled(bool p_enabled) override {}
virtual void set_reuse_address_enabled(bool p_enabled) override {}
virtual Error join_multicast_group(const IPAddress &p_multi_address, const String &p_if_name) override { return ERR_UNAVAILABLE; }
virtual Error leave_multicast_group(const IPAddress &p_multi_address, const String &p_if_name) override { return ERR_UNAVAILABLE; }
NetSocketWeb() {}
};
#endif // NET_SOCKET_WEB_H

View file

@ -33,6 +33,8 @@
#include "api/javascript_bridge_singleton.h"
#include "display_server_web.h"
#include "godot_js.h"
#include "ip_web.h"
#include "net_socket_web.h"
#include "core/config/project_settings.h"
#include "core/debugger/engine_debugger.h"
@ -53,6 +55,8 @@ void OS_Web::alert(const String &p_alert, const String &p_title) {
// Lifecycle
void OS_Web::initialize() {
OS_Unix::initialize_core();
IPWeb::make_default();
NetSocketWeb::make_default();
DisplayServerWeb::register_web_driver();
}
@ -105,7 +109,7 @@ Error OS_Web::execute(const String &p_path, const List<String> &p_arguments, Str
return create_process(p_path, p_arguments);
}
Dictionary OS_Web::execute_with_pipe(const String &p_path, const List<String> &p_arguments) {
Dictionary OS_Web::execute_with_pipe(const String &p_path, const List<String> &p_arguments, bool p_blocking) {
ERR_FAIL_V_MSG(Dictionary(), "OS::execute_with_pipe is not available on the Web platform.");
}
@ -178,23 +182,9 @@ void OS_Web::vibrate_handheld(int p_duration_ms, float p_amplitude) {
godot_js_input_vibrate_handheld(p_duration_ms);
}
String OS_Web::get_user_data_dir() const {
String OS_Web::get_user_data_dir(const String &p_user_dir) const {
String userfs = "/userfs";
String appname = get_safe_dir_name(GLOBAL_GET("application/config/name"));
if (!appname.is_empty()) {
bool use_custom_dir = GLOBAL_GET("application/config/use_custom_user_dir");
if (use_custom_dir) {
String custom_dir = get_safe_dir_name(GLOBAL_GET("application/config/custom_user_dir_name"), true);
if (custom_dir.is_empty()) {
custom_dir = appname;
}
return userfs.path_join(custom_dir).replace("\\", "/");
} else {
return userfs.path_join(get_godot_dir_name()).path_join("app_userdata").path_join(appname).replace("\\", "/");
}
}
return userfs.path_join(get_godot_dir_name()).path_join("app_userdata").path_join("[unnamed project]");
return userfs.path_join(p_user_dir).replace("\\", "/");
}
String OS_Web::get_cache_path() const {
@ -224,6 +214,18 @@ void OS_Web::file_access_close_callback(const String &p_file, int p_flags) {
}
}
void OS_Web::dir_access_remove_callback(const String &p_file) {
OS_Web *os = OS_Web::get_singleton();
bool is_file_persistent = p_file.begins_with("/userfs");
#ifdef TOOLS_ENABLED
// Hack for editor persistence (can we track).
is_file_persistent = is_file_persistent || p_file.begins_with("/home/web_user/");
#endif
if (is_file_persistent) {
os->idb_needs_sync = true;
}
}
void OS_Web::update_pwa_state_callback() {
if (OS_Web::get_singleton()) {
OS_Web::get_singleton()->pwa_is_waiting = true;
@ -275,6 +277,7 @@ OS_Web::OS_Web() {
if (AudioDriverWeb::is_available()) {
audio_drivers.push_back(memnew(AudioDriverWorklet));
audio_drivers.push_back(memnew(AudioDriverScriptProcessor));
}
for (AudioDriverWeb *audio_driver : audio_drivers) {
AudioDriverManager::add_driver(audio_driver);
@ -287,4 +290,5 @@ OS_Web::OS_Web() {
_set_logger(memnew(CompositeLogger(loggers)));
FileAccessUnix::close_notification_func = file_access_close_callback;
DirAccessUnix::remove_notification_func = dir_access_remove_callback;
}

View file

@ -32,6 +32,7 @@
#define OS_WEB_H
#include "audio_driver_web.h"
#include "webmidi_driver.h"
#include "godot_js.h"
@ -45,6 +46,8 @@ class OS_Web : public OS_Unix {
MainLoop *main_loop = nullptr;
List<AudioDriverWeb *> audio_drivers;
MIDIDriverWebMidi midi_driver;
bool idb_is_syncing = false;
bool idb_available = false;
bool idb_needs_sync = false;
@ -53,6 +56,7 @@ class OS_Web : public OS_Unix {
WASM_EXPORT static void main_loop_callback();
WASM_EXPORT static void file_access_close_callback(const String &p_file, int p_flags);
WASM_EXPORT static void dir_access_remove_callback(const String &p_file);
WASM_EXPORT static void fs_sync_callback();
WASM_EXPORT static void update_pwa_state_callback();
@ -80,7 +84,7 @@ public:
bool main_loop_iterate();
Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override;
Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments) override;
Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments, bool p_blocking = true) override;
Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override;
Error kill(const ProcessID &p_pid) override;
int get_process_id() const override;
@ -103,7 +107,7 @@ public:
String get_cache_path() const override;
String get_config_path() const override;
String get_data_path() const override;
String get_user_data_dir() const override;
String get_user_data_dir(const String &p_user_dir) const override;
bool is_userfs_persistent() const override;

File diff suppressed because it is too large Load diff

View file

@ -11,14 +11,14 @@
"format": "npm run lint -- --fix"
},
"devDependencies": {
"@eslint/js": "^9.3.0",
"@html-eslint/eslint-plugin": "^0.24.1",
"@html-eslint/parser": "^0.24.1",
"@stylistic/eslint-plugin": "^2.1.0",
"eslint": "^9.3.0",
"@eslint/js": "^9.12.0",
"@html-eslint/eslint-plugin": "^0.27.0",
"@html-eslint/parser": "^0.27.0",
"@stylistic/eslint-plugin": "^2.9.0",
"eslint": "^9.15.0",
"eslint-plugin-html": "^8.1.1",
"espree": "^10.0.1",
"globals": "^15.3.0",
"globals": "^15.9.0",
"jsdoc": "^4.0.3"
}
}

View file

@ -6,7 +6,7 @@ import os
import socket
import subprocess
import sys
from http.server import HTTPServer, SimpleHTTPRequestHandler, test # type: ignore
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
@ -38,12 +38,24 @@ def shell_open(url):
def serve(root, port, run_browser):
os.chdir(root)
address = ("", port)
httpd = DualStackServer(address, CORSRequestHandler)
url = f"http://127.0.0.1:{port}"
if run_browser:
# Open the served page in the user's default browser.
print("Opening the served URL in the default browser (use `--no-browser` or `-n` to disable this).")
shell_open(f"http://127.0.0.1:{port}")
print(f"Opening the served URL in the default browser (use `--no-browser` or `-n` to disable this): {url}")
shell_open(url)
else:
print(f"Serving at: {url}")
test(CORSRequestHandler, DualStackServer, port=port)
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nKeyboard interrupt received, stopping server.")
finally:
# Clean-up server
httpd.server_close()
if __name__ == "__main__":

View file

@ -38,6 +38,10 @@
#include "scene/main/scene_tree.h"
#include "scene/main/window.h" // SceneTree only forward declares it.
#ifdef TOOLS_ENABLED
#include "editor/web_tools_editor_plugin.h"
#endif
#include <emscripten/emscripten.h>
#include <stdlib.h>
@ -104,6 +108,10 @@ void main_loop_callback() {
extern EMSCRIPTEN_KEEPALIVE int godot_web_main(int argc, char *argv[]) {
os = new OS_Web();
#ifdef TOOLS_ENABLED
WebToolsEditorPlugin::initialize();
#endif
// We must override main when testing is enabled
TEST_MAIN_OVERRIDE

View file

@ -0,0 +1,98 @@
/**************************************************************************/
/* webmidi_driver.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 "webmidi_driver.h"
#ifdef PROXY_TO_PTHREAD_ENABLED
#include "core/object/callable_method_pointer.h"
#endif
MIDIDriverWebMidi *MIDIDriverWebMidi::get_singleton() {
return static_cast<MIDIDriverWebMidi *>(MIDIDriver::get_singleton());
}
Error MIDIDriverWebMidi::open() {
Error error = (Error)godot_js_webmidi_open_midi_inputs(&MIDIDriverWebMidi::set_input_names_callback, &MIDIDriverWebMidi::on_midi_message, _event_buffer, MIDIDriverWebMidi::MAX_EVENT_BUFFER_LENGTH);
if (error == ERR_UNAVAILABLE) {
ERR_PRINT("Web MIDI is not supported on this browser.");
}
return error;
}
void MIDIDriverWebMidi::close() {
get_singleton()->connected_input_names.clear();
godot_js_webmidi_close_midi_inputs();
}
MIDIDriverWebMidi::~MIDIDriverWebMidi() {
close();
}
void MIDIDriverWebMidi::set_input_names_callback(int p_size, const char **p_input_names) {
Vector<String> input_names;
for (int i = 0; i < p_size; i++) {
input_names.append(String::utf8(p_input_names[i]));
}
#ifdef PROXY_TO_PTHREAD_ENABLED
if (!Thread::is_main_thread()) {
callable_mp_static(MIDIDriverWebMidi::_set_input_names_callback).call_deferred(input_names);
return;
}
#endif
_set_input_names_callback(input_names);
}
void MIDIDriverWebMidi::_set_input_names_callback(const Vector<String> &p_input_names) {
get_singleton()->connected_input_names.clear();
for (const String &input_name : p_input_names) {
get_singleton()->connected_input_names.push_back(input_name);
}
}
void MIDIDriverWebMidi::on_midi_message(int p_device_index, int p_status, const uint8_t *p_data, int p_data_len) {
PackedByteArray data;
data.resize(p_data_len);
uint8_t *data_ptr = data.ptrw();
for (int i = 0; i < p_data_len; i++) {
data_ptr[i] = p_data[i];
}
#ifdef PROXY_TO_PTHREAD_ENABLED
if (!Thread::is_main_thread()) {
callable_mp_static(MIDIDriverWebMidi::_on_midi_message).call_deferred(p_device_index, p_status, data, p_data_len);
return;
}
#endif
_on_midi_message(p_device_index, p_status, data, p_data_len);
}
void MIDIDriverWebMidi::_on_midi_message(int p_device_index, int p_status, const PackedByteArray &p_data, int p_data_len) {
MIDIDriver::send_event(p_device_index, p_status, p_data.ptr(), p_data_len);
}

View file

@ -0,0 +1,61 @@
/**************************************************************************/
/* webmidi_driver.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* 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 WEBMIDI_DRIVER_H
#define WEBMIDI_DRIVER_H
#include "core/os/midi_driver.h"
#include "godot_js.h"
#include "godot_midi.h"
class MIDIDriverWebMidi : public MIDIDriver {
private:
static const int MAX_EVENT_BUFFER_LENGTH = 2;
uint8_t _event_buffer[MAX_EVENT_BUFFER_LENGTH];
public:
// Override return type to make writing static callbacks less tedious.
static MIDIDriverWebMidi *get_singleton();
virtual Error open() override;
virtual void close() override final;
MIDIDriverWebMidi() = default;
virtual ~MIDIDriverWebMidi();
WASM_EXPORT static void set_input_names_callback(int p_size, const char **p_input_names);
static void _set_input_names_callback(const Vector<String> &p_input_names);
WASM_EXPORT static void on_midi_message(int p_device_index, int p_status, const uint8_t *p_data, int p_data_len);
static void _on_midi_message(int p_device_index, int p_status, const PackedByteArray &p_data, int p_data_len);
};
#endif // WEBMIDI_DRIVER_H