diff options
52 files changed, 2367 insertions, 1566 deletions
diff --git a/.appveyor.yml b/.appveyor.yml index 3f6e932050..e2105be6c0 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -39,3 +39,11 @@ before_build: build_script: - scons platform=%GD_PLATFORM% target=%TARGET% tools=%TOOLS% %OPTIONS% %EXTRA_ARGS% + +after_build: + - 7z a godot_build.zip bin\*.exe + +artifacts: + - path: godot_build.zip + name: Build + type: zip diff --git a/.travis.yml b/.travis.yml index 8cfd7a1a7f..8878935e52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -100,15 +100,15 @@ matrix: packages: - *linux_deps -# - name: Javascript export template (release, emscripten latest) -# stage: build -# env: PLATFORM=javascript TOOLS=no TARGET=release CACHE_NAME=${PLATFORM}-emcc-latest EXTRA_ARGS="use_closure_compiler=yes" -# os: linux -# compiler: clang -# addons: -# apt: -# packages: -# - *linux_deps + - name: Javascript export template (release, emscripten latest) + stage: build + env: PLATFORM=javascript TOOLS=no TARGET=release CACHE_NAME=${PLATFORM}-emcc-latest EXTRA_ARGS="use_closure_compiler=yes" + os: linux + compiler: clang + addons: + apt: + packages: + - *linux_deps before_install: - eval "${MATRIX_EVAL}" diff --git a/core/input/godotcontrollerdb.txt b/core/input/godotcontrollerdb.txt index adc3649d64..51ddda1e4e 100644 --- a/core/input/godotcontrollerdb.txt +++ b/core/input/godotcontrollerdb.txt @@ -4,6 +4,9 @@ # Windows __XINPUT_DEVICE__,XInput Gamepad,a:b12,b:b13,x:b14,y:b15,start:b4,back:b5,leftstick:b6,rightstick:b7,leftshoulder:b8,rightshoulder:b9,dpup:b0,dpdown:b1,dpleft:b2,dpright:b3,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,platform:Windows, +# Android +Default Android Gamepad,Default Controller,leftx:a0,lefty:a1,dpdown:h0.4,rightstick:b8,rightshoulder:b10,rightx:a2,start:b6,righty:a3,dpleft:h0.8,lefttrigger:a4,x:b2,dpup:h0.1,back:b4,leftstick:b7,leftshoulder:b9,y:b3,a:b0,dpright:h0.2,righttrigger:a5,b:b1,platform:Android, + # Javascript Default HTML5 Gamepad, Default Mapping,leftx:a0,lefty:a1,dpdown:b13,rightstick:b11,rightshoulder:b5,rightx:a2,start:b9,righty:a3,dpleft:b14,lefttrigger:a6,x:b2,dpup:b12,back:b8,leftstick:b10,leftshoulder:b4,y:b3,a:b0,dpright:b15,righttrigger:a7,b:b1,platform:Javascript, c2a94d6963726f736f66742058626f78,Wireless X360 Controller,leftx:a0,lefty:a1,dpdown:b14,rightstick:b10,rightshoulder:b5,rightx:a3,start:b7,righty:a4,dpleft:b11,lefttrigger:a2,x:b2,dpup:b13,back:b6,leftstick:b9,leftshoulder:b4,y:b3,a:b0,dpright:b12,righttrigger:a5,b:b1,platform:Javascript, diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 222b70c9c7..602ad43103 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -77,9 +77,6 @@ <member name="ProjectSettings" type="ProjectSettings" setter="" getter=""> The [ProjectSettings] singleton. </member> - <member name="RenderingDevice" type="RenderingDevice" setter="" getter=""> - The [RenderingDevice] singleton. - </member> <member name="RenderingServer" type="RenderingServer" setter="" getter=""> The [RenderingServer] singleton. </member> diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml index 569da5c58b..59450b7ea2 100644 --- a/doc/classes/PopupMenu.xml +++ b/doc/classes/PopupMenu.xml @@ -242,6 +242,12 @@ Removes all items from the [PopupMenu]. </description> </method> + <method name="get_current_index" qualifiers="const"> + <return type="int"> + </return> + <description> + </description> + </method> <method name="get_item_accelerator" qualifiers="const"> <return type="int"> </return> diff --git a/doc/classes/String.xml b/doc/classes/String.xml index 03f62732c4..0dd6923129 100644 --- a/doc/classes/String.xml +++ b/doc/classes/String.xml @@ -620,6 +620,19 @@ Returns [code]true[/code] if this string contains a valid IP address. </description> </method> + <method name="join"> + <return type="String"> + </return> + <argument index="0" name="parts" type="PackedStringArray"> + </argument> + <description> + Return a [String] which is the concatenation of the [code]parts[/code]. The separator between elements is the string providing this method. + Example: + [codeblock] + print(", ".join(["One", "Two", "Three", "Four"])) + [/codeblock] + </description> + </method> <method name="json_escape"> <return type="String"> </return> @@ -837,19 +850,6 @@ Returns a copy of the string with characters removed from the right. </description> </method> - <method name="join"> - <return type="String"> - </return> - <argument index="0" name="parts" type="PackedStringArray"> - </argument> - <description> - Return a [String] which is the concatenation of the [code]parts[/code]. The separator between elements is the string providing this method. - Example: - [codeblock] - print(", ".join(["One", "Two", "Three", "Four"])) - [/codeblock] - </description> - </method> <method name="sha1_buffer"> <return type="PackedByteArray"> </return> diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index bb2d355bf6..e553518b39 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -343,26 +343,26 @@ Select all the text. </description> </method> - <method name="set_line_as_hidden"> + <method name="set_line"> <return type="void"> </return> <argument index="0" name="line" type="int"> </argument> - <argument index="1" name="enable" type="bool"> + <argument index="1" name="new_text" type="String"> </argument> <description> - If [code]true[/code], hides the line of the specified index. + Sets the text for a specific line. </description> </method> - <method name="set_line"> + <method name="set_line_as_hidden"> <return type="void"> </return> <argument index="0" name="line" type="int"> </argument> - <argument index="1" name="new_text" type="String"> + <argument index="1" name="enable" type="bool"> </argument> <description> - Sets the text for a specific line. + If [code]true[/code], hides the line of the specified index. </description> </method> <method name="toggle_fold_line"> diff --git a/doc/classes/TreeItem.xml b/doc/classes/TreeItem.xml index a8a17370c2..126d6b4180 100644 --- a/doc/classes/TreeItem.xml +++ b/doc/classes/TreeItem.xml @@ -249,6 +249,14 @@ <description> </description> </method> + <method name="get_suffix" qualifiers="const"> + <return type="String"> + </return> + <argument index="0" name="column" type="int"> + </argument> + <description> + </description> + </method> <method name="get_text" qualifiers="const"> <return type="String"> </return> @@ -572,6 +580,16 @@ If [code]true[/code], the given column is selectable. </description> </method> + <method name="set_suffix"> + <return type="void"> + </return> + <argument index="0" name="column" type="int"> + </argument> + <argument index="1" name="text" type="String"> + </argument> + <description> + </description> + </method> <method name="set_text"> <return type="void"> </return> diff --git a/drivers/dummy/rasterizer_dummy.h b/drivers/dummy/rasterizer_dummy.h index 0bcfed2dcf..ab62c34f16 100644 --- a/drivers/dummy/rasterizer_dummy.h +++ b/drivers/dummy/rasterizer_dummy.h @@ -78,10 +78,11 @@ public: #endif void environment_set_glow(RID p_env, bool p_enable, int p_level_flags, float p_intensity, float p_strength, float p_mix, float p_bloom_threshold, RS::EnvironmentGlowBlendMode p_blend_mode, float p_hdr_bleed_threshold, float p_hdr_bleed_scale, float p_hdr_luminance_cap) {} - + virtual void environment_glow_set_use_bicubic_upscale(bool p_enable) {} void environment_set_fog(RID p_env, bool p_enable, float p_begin, float p_end, RID p_gradient_texture) {} - void environment_set_ssr(RID p_env, bool p_enable, int p_max_steps, float p_fade_int, float p_fade_out, float p_depth_tolerance, bool p_roughness) {} + void environment_set_ssr(RID p_env, bool p_enable, int p_max_steps, float p_fade_int, float p_fade_out, float p_depth_tolerance) {} + virtual void environment_set_ssr_roughness_quality(RS::EnvironmentSSRRoughnessQuality p_quality) {} virtual void environment_set_ssao(RID p_env, bool p_enable, float p_radius, float p_intensity, float p_bias, float p_light_affect, float p_ao_channel_affect, RS::EnvironmentSSAOBlur p_blur, float p_bilateral_sharpness) {} virtual void environment_set_ssao_quality(RS::EnvironmentSSAOQuality p_quality, bool p_half_size) {} @@ -105,9 +106,12 @@ public: virtual void camera_effects_set_dof_blur(RID p_camera_effects, bool p_far_enable, float p_far_distance, float p_far_transition, bool p_near_enable, float p_near_distance, float p_near_transition, float p_amount) {} virtual void camera_effects_set_custom_exposure(RID p_camera_effects, bool p_enable, float p_exposure) {} + virtual void shadows_quality_set(RS::ShadowQuality p_quality) {} + virtual void directional_shadow_quality_set(RS::ShadowQuality p_quality) {} + RID light_instance_create(RID p_light) { return RID(); } void light_instance_set_transform(RID p_light_instance, const Transform &p_transform) {} - void light_instance_set_shadow_transform(RID p_light_instance, const CameraMatrix &p_projection, const Transform &p_transform, float p_far, float p_split, int p_pass, float p_bias_scale = 1.0) {} + void light_instance_set_shadow_transform(RID p_light_instance, const CameraMatrix &p_projection, const Transform &p_transform, float p_far, float p_split, int p_pass, float p_shadow_texel_size, float p_bias_scale = 1.0, float p_range_begin = 0, const Vector2 &p_uv_scale = Vector2()) {} void light_instance_mark_visible(RID p_light_instance) {} RID reflection_atlas_create() { return RID(); } @@ -121,13 +125,16 @@ public: bool reflection_probe_instance_begin_render(RID p_instance, RID p_reflection_atlas) { return false; } bool reflection_probe_instance_postprocess_step(RID p_instance) { return true; } + virtual RID decal_instance_create(RID p_decal) { return RID(); } + virtual void decal_instance_set_transform(RID p_decal, const Transform &p_transform) {} + virtual RID gi_probe_instance_create(RID p_gi_probe) { return RID(); } void gi_probe_instance_set_light_data(RID p_probe, RID p_base, RID p_data) {} void gi_probe_instance_set_transform_to_data(RID p_probe, const Transform &p_xform) {} virtual bool gi_probe_needs_update(RID p_probe) const { return false; } virtual void gi_probe_update(RID p_probe, bool p_update_light_instances, const Vector<RID> &p_light_instances, int p_dynamic_object_count, InstanceBase **p_dynamic_objects) {} - virtual void render_scene(RID p_render_buffers, const Transform &p_cam_transform, const CameraMatrix &p_cam_projection, bool p_cam_ortogonal, InstanceBase **p_cull_result, int p_cull_count, RID *p_light_cull_result, int p_light_cull_count, RID *p_reflection_probe_cull_result, int p_reflection_probe_cull_count, RID *p_gi_probe_cull_result, int p_gi_probe_cull_count, RID p_environment, RID p_camera_effects, RID p_shadow_atlas, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass) {} + virtual void render_scene(RID p_render_buffers, const Transform &p_cam_transform, const CameraMatrix &p_cam_projection, bool p_cam_ortogonal, InstanceBase **p_cull_result, int p_cull_count, RID *p_light_cull_result, int p_light_cull_count, RID *p_reflection_probe_cull_result, int p_reflection_probe_cull_count, RID *p_gi_probe_cull_result, int p_gi_probe_cull_count, RID *p_decal_cull_result, int p_decal_cull_count, RID p_environment, RID p_camera_effects, RID p_shadow_atlas, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass) {} void render_shadow(RID p_light, RID p_shadow_atlas, int p_pass, InstanceBase **p_cull_result, int p_cull_count) {} virtual void render_material(const Transform &p_cam_transform, const CameraMatrix &p_cam_projection, bool p_cam_ortogonal, InstanceBase **p_cull_result, int p_cull_count, RID p_framebuffer, const Rect2i &p_region) {} @@ -136,11 +143,14 @@ public: void set_debug_draw_mode(RS::ViewportDebugDraw p_debug_draw) {} virtual RID render_buffers_create() { return RID(); } - virtual void render_buffers_configure(RID p_render_buffers, RID p_render_target, int p_width, int p_height, RS::ViewportMSAA p_msaa) {} + virtual void render_buffers_configure(RID p_render_buffers, RID p_render_target, int p_width, int p_height, RS::ViewportMSAA p_msaa, RS::ViewportScreenSpaceAA p_screen_space_aa) {} virtual void screen_space_roughness_limiter_set_active(bool p_enable, float p_curve) {} virtual bool screen_space_roughness_limiter_is_active() const { return false; } + virtual void sub_surface_scattering_set_quality(RS::SubSurfaceScatteringQuality p_quality) {} + virtual void sub_surface_scattering_set_scale(float p_scale, float p_depth_scale) {} + bool free(RID p_rid) { return true; } virtual void update() {} @@ -217,6 +227,9 @@ public: virtual void texture_set_force_redraw_if_visible(RID p_texture, bool p_enable) {} virtual Size2 texture_size_with_proxy(RID p_proxy) { return Size2(); } + virtual void texture_add_to_decal_atlas(RID p_texture, bool p_panorama_to_dp = false) {} + virtual void texture_remove_from_decal_atlas(RID p_texture, bool p_panorama_to_dp = false) {} + #if 0 RID texture_create() { @@ -345,6 +358,7 @@ public: bool material_is_animated(RID p_material) { return false; } bool material_casts_shadows(RID p_material) { return false; } + virtual void material_get_instance_shader_parameters(RID p_material, List<InstanceShaderParam> *r_parameters) {} void material_update_dependency(RID p_material, RasterizerScene::InstanceBase *p_instance) {} /* MESH API */ @@ -608,6 +622,21 @@ public: virtual void base_update_dependency(RID p_base, RasterizerScene::InstanceBase *p_instance) {} virtual void skeleton_update_dependency(RID p_base, RasterizerScene::InstanceBase *p_instance) {} + /* DECAL API */ + + virtual RID decal_create() { return RID(); } + virtual void decal_set_extents(RID p_decal, const Vector3 &p_extents) {} + virtual void decal_set_texture(RID p_decal, RS::DecalTexture p_type, RID p_texture) {} + virtual void decal_set_emission_energy(RID p_decal, float p_energy) {} + virtual void decal_set_albedo_mix(RID p_decal, float p_mix) {} + virtual void decal_set_modulate(RID p_decal, const Color &p_modulate) {} + virtual void decal_set_cull_mask(RID p_decal, uint32_t p_layers) {} + virtual void decal_set_distance_fade(RID p_decal, bool p_enabled, float p_begin, float p_length) {} + virtual void decal_set_fade(RID p_decal, float p_above, float p_below) {} + virtual void decal_set_normal_fade(RID p_decal, float p_fade) {} + + virtual AABB decal_get_aabb(RID p_decal) const { return AABB(); } + /* GI PROBE API */ RID gi_probe_create() { return RID(); } @@ -757,6 +786,24 @@ public: int particles_get_draw_passes(RID p_particles) const { return 0; } RID particles_get_draw_pass_mesh(RID p_particles, int p_pass) const { return RID(); } + /* GLOBAL VARIABLES */ + + virtual void global_variable_add(const StringName &p_name, RS::GlobalVariableType p_type, const Variant &p_value) {} + virtual void global_variable_remove(const StringName &p_name) {} + virtual Vector<StringName> global_variable_get_list() const { return Vector<StringName>(); } + + virtual void global_variable_set(const StringName &p_name, const Variant &p_value) {} + virtual void global_variable_set_override(const StringName &p_name, const Variant &p_value) {} + virtual Variant global_variable_get(const StringName &p_name) const { return Variant(); } + virtual RS::GlobalVariableType global_variable_get_type(const StringName &p_name) const { return RS::GLOBAL_VAR_TYPE_MAX; } + + virtual void global_variables_load_settings(bool p_load_textures = true) {} + virtual void global_variables_clear() {} + + virtual int32_t global_variables_instance_allocate(RID p_instance) { return 0; } + virtual void global_variables_instance_free(RID p_instance) {} + virtual void global_variables_instance_update(RID p_instance, int p_index, const Variant &p_value) {} + virtual bool particles_is_inactive(RID p_particles) const { return false; } /* RENDER TARGET */ @@ -871,7 +918,12 @@ public: virtual void prepare_for_blitting_render_targets() {} virtual void blit_render_targets_to_screen(int p_screen, const BlitToScreen *p_render_targets, int p_amount) {} - void end_frame(bool p_swap_buffers) { OS::get_singleton()->swap_buffers(); } + void end_frame(bool p_swap_buffers) { + if (p_swap_buffers) { + DisplayServer::get_singleton()->swap_buffers(); + } + } + void finalize() {} static Error is_viable() { diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp index 3302b50103..7551214336 100644 --- a/editor/debugger/editor_debugger_node.cpp +++ b/editor/debugger/editor_debugger_node.cpp @@ -261,10 +261,12 @@ void EditorDebuggerNode::_notification(int p_what) { debugger_button->set_icon(Ref<Texture2D>()); } else { debugger_button->set_text(TTR("Debugger") + " (" + itos(error_count + warning_count) + ")"); - if (error_count == 0) { - debugger_button->set_icon(get_theme_icon("Warning", "EditorIcons")); - } else { + if (error_count >= 1 && warning_count >= 1) { + debugger_button->set_icon(get_theme_icon("ErrorWarning", "EditorIcons")); + } else if (error_count >= 1) { debugger_button->set_icon(get_theme_icon("Error", "EditorIcons")); + } else { + debugger_button->set_icon(get_theme_icon("Warning", "EditorIcons")); } } last_error_count = error_count; diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index 0fa62253c3..af79de2991 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -131,10 +131,12 @@ void ScriptEditorDebugger::update_tabs() { tabs->set_tab_icon(errors_tab->get_index(), Ref<Texture2D>()); } else { errors_tab->set_name(TTR("Errors") + " (" + itos(error_count + warning_count) + ")"); - if (error_count == 0) { - tabs->set_tab_icon(errors_tab->get_index(), get_theme_icon("Warning", "EditorIcons")); - } else { + if (error_count >= 1 && warning_count >= 1) { + tabs->set_tab_icon(errors_tab->get_index(), get_theme_icon("ErrorWarning", "EditorIcons")); + } else if (error_count >= 1) { tabs->set_tab_icon(errors_tab->get_index(), get_theme_icon("Error", "EditorIcons")); + } else { + tabs->set_tab_icon(errors_tab->get_index(), get_theme_icon("Warning", "EditorIcons")); } } } diff --git a/editor/editor_log.cpp b/editor/editor_log.cpp index c89a7bcf23..78aed96363 100644 --- a/editor/editor_log.cpp +++ b/editor/editor_log.cpp @@ -63,11 +63,13 @@ void EditorLog::_notification(int p_what) { //button->set_icon(get_icon("Console","EditorIcons")); log->add_theme_font_override("normal_font", get_theme_font("output_source", "EditorFonts")); + log->add_theme_color_override("selection_color", get_theme_color("accent_color", "Editor") * Color(1, 1, 1, 0.4)); } else if (p_what == NOTIFICATION_THEME_CHANGED) { Ref<DynamicFont> df_output_code = get_theme_font("output_source", "EditorFonts"); if (df_output_code.is_valid()) { if (log != nullptr) { log->add_theme_font_override("normal_font", get_theme_font("output_source", "EditorFonts")); + log->add_theme_color_override("selection_color", get_theme_color("accent_color", "Editor") * Color(1, 1, 1, 0.4)); } } } diff --git a/editor/icons/ErrorWarning.svg b/editor/icons/ErrorWarning.svg new file mode 100644 index 0000000000..72b5037e50 --- /dev/null +++ b/editor/icons/ErrorWarning.svg @@ -0,0 +1 @@ +<svg height="8" viewBox="0 0 8 8" width="8" xmlns="http://www.w3.org/2000/svg"><path d="m4 0c-2.216 0-4 1.784-4 4s1.784 4 4 4z" fill="#ff5d5d"/><path d="m4 .00000003c2.216 0 4 1.78399997 4 3.99999997s-1.784 4-4 4z" fill="#ffdd65"/></svg>
\ No newline at end of file diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 5ee5fbcf9c..6e00e3c291 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -97,8 +97,8 @@ void SceneTreeDock::_unhandled_key_input(Ref<InputEvent> p_event) { _tool_selected(TOOL_DUPLICATE); } else if (ED_IS_SHORTCUT("scene_tree/attach_script", p_event)) { _tool_selected(TOOL_ATTACH_SCRIPT); - } else if (ED_IS_SHORTCUT("scene_tree/clear_script", p_event)) { - _tool_selected(TOOL_CLEAR_SCRIPT); + } else if (ED_IS_SHORTCUT("scene_tree/detach_script", p_event)) { + _tool_selected(TOOL_DETACH_SCRIPT); } else if (ED_IS_SHORTCUT("scene_tree/move_up", p_event)) { _tool_selected(TOOL_MOVE_UP); } else if (ED_IS_SHORTCUT("scene_tree/move_down", p_event)) { @@ -426,7 +426,7 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { case TOOL_ATTACH_SCRIPT: { attach_script_to_selected(false); } break; - case TOOL_CLEAR_SCRIPT: { + case TOOL_DETACH_SCRIPT: { if (!profile_allow_script_editing) { break; @@ -437,7 +437,7 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { if (selection.empty()) return; - editor_data->get_undo_redo().create_action(TTR("Clear Script")); + editor_data->get_undo_redo().create_action(TTR("Detach Script")); editor_data->get_undo_redo().add_do_method(editor, "push_item", (Script *)nullptr); for (int i = 0; i < selection.size(); i++) { @@ -1071,7 +1071,7 @@ void SceneTreeDock::_notification(int p_what) { button_add->set_icon(get_theme_icon("Add", "EditorIcons")); button_instance->set_icon(get_theme_icon("Instance", "EditorIcons")); button_create_script->set_icon(get_theme_icon("ScriptCreate", "EditorIcons")); - button_clear_script->set_icon(get_theme_icon("ScriptRemove", "EditorIcons")); + button_detach_script->set_icon(get_theme_icon("ScriptRemove", "EditorIcons")); filter->set_right_icon(get_theme_icon("Search", "EditorIcons")); filter->set_clear_button_enabled(true); @@ -1148,7 +1148,7 @@ void SceneTreeDock::_notification(int p_what) { button_add->set_icon(get_theme_icon("Add", "EditorIcons")); button_instance->set_icon(get_theme_icon("Instance", "EditorIcons")); button_create_script->set_icon(get_theme_icon("ScriptCreate", "EditorIcons")); - button_clear_script->set_icon(get_theme_icon("ScriptRemove", "EditorIcons")); + button_detach_script->set_icon(get_theme_icon("ScriptRemove", "EditorIcons")); filter->set_right_icon(get_theme_icon("Search", "EditorIcons")); filter->set_clear_button_enabled(true); @@ -1883,18 +1883,18 @@ void SceneTreeDock::_update_script_button() { if (!profile_allow_script_editing) { button_create_script->hide(); - button_clear_script->hide(); + button_detach_script->hide(); } else if (EditorNode::get_singleton()->get_editor_selection()->get_selection().size() == 0) { button_create_script->hide(); - button_clear_script->hide(); + button_detach_script->hide(); } else if (EditorNode::get_singleton()->get_editor_selection()->get_selection().size() == 1) { Node *n = EditorNode::get_singleton()->get_editor_selection()->get_selected_node_list()[0]; if (n->get_script().is_null()) { button_create_script->show(); - button_clear_script->hide(); + button_detach_script->hide(); } else { button_create_script->hide(); - button_clear_script->show(); + button_detach_script->show(); } } else { button_create_script->hide(); @@ -1902,11 +1902,11 @@ void SceneTreeDock::_update_script_button() { for (int i = 0; i < selection.size(); i++) { Node *n = Object::cast_to<Node>(selection[i]); if (!n->get_script().is_null()) { - button_clear_script->show(); + button_detach_script->show(); return; } } - button_clear_script->hide(); + button_detach_script->hide(); } } @@ -2458,7 +2458,7 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) { } if (existing_script.is_valid() && existing_script_removable) { add_separator = true; - menu->add_icon_shortcut(get_theme_icon("ScriptRemove", "EditorIcons"), ED_GET_SHORTCUT("scene_tree/clear_script"), TOOL_CLEAR_SCRIPT); + menu->add_icon_shortcut(get_theme_icon("ScriptRemove", "EditorIcons"), ED_GET_SHORTCUT("scene_tree/detach_script"), TOOL_DETACH_SCRIPT); } else if (full_selection.size() > 1) { bool script_exists = false; for (List<Node *>::Element *E = full_selection.front(); E; E = E->next()) { @@ -2470,7 +2470,7 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) { if (script_exists) { add_separator = true; - menu->add_icon_shortcut(get_theme_icon("ScriptRemove", "EditorIcons"), ED_GET_SHORTCUT("scene_tree/clear_script"), TOOL_CLEAR_SCRIPT); + menu->add_icon_shortcut(get_theme_icon("ScriptRemove", "EditorIcons"), ED_GET_SHORTCUT("scene_tree/detach_script"), TOOL_DETACH_SCRIPT); } } @@ -2816,7 +2816,7 @@ SceneTreeDock::SceneTreeDock(EditorNode *p_editor, Node *p_scene_root, EditorSel ED_SHORTCUT("scene_tree/change_node_type", TTR("Change Type")); ED_SHORTCUT("scene_tree/attach_script", TTR("Attach Script")); ED_SHORTCUT("scene_tree/extend_script", TTR("Extend Script")); - ED_SHORTCUT("scene_tree/clear_script", TTR("Clear Script")); + ED_SHORTCUT("scene_tree/detach_script", TTR("Detach Script")); ED_SHORTCUT("scene_tree/move_up", TTR("Move Up"), KEY_MASK_CMD | KEY_UP); ED_SHORTCUT("scene_tree/move_down", TTR("Move Down"), KEY_MASK_CMD | KEY_DOWN); ED_SHORTCUT("scene_tree/duplicate", TTR("Duplicate"), KEY_MASK_CMD | KEY_D); @@ -2851,17 +2851,17 @@ SceneTreeDock::SceneTreeDock(EditorNode *p_editor, Node *p_scene_root, EditorSel button_create_script = memnew(ToolButton); button_create_script->connect("pressed", callable_mp(this, &SceneTreeDock::_tool_selected), make_binds(TOOL_ATTACH_SCRIPT, false)); - button_create_script->set_tooltip(TTR("Attach a new or existing script for the selected node.")); + button_create_script->set_tooltip(TTR("Attach a new or existing script to the selected node.")); button_create_script->set_shortcut(ED_GET_SHORTCUT("scene_tree/attach_script")); filter_hbc->add_child(button_create_script); button_create_script->hide(); - button_clear_script = memnew(ToolButton); - button_clear_script->connect("pressed", callable_mp(this, &SceneTreeDock::_tool_selected), make_binds(TOOL_CLEAR_SCRIPT, false)); - button_clear_script->set_tooltip(TTR("Clear a script for the selected node.")); - button_clear_script->set_shortcut(ED_GET_SHORTCUT("scene_tree/clear_script")); - filter_hbc->add_child(button_clear_script); - button_clear_script->hide(); + button_detach_script = memnew(ToolButton); + button_detach_script->connect("pressed", callable_mp(this, &SceneTreeDock::_tool_selected), make_binds(TOOL_DETACH_SCRIPT, false)); + button_detach_script->set_tooltip(TTR("Detach the script from the selected node.")); + button_detach_script->set_shortcut(ED_GET_SHORTCUT("scene_tree/detach_script")); + filter_hbc->add_child(button_detach_script); + button_detach_script->hide(); button_hb = memnew(HBoxContainer); vbc->add_child(button_hb); diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h index 31ef1ce7d0..00b95c9853 100644 --- a/editor/scene_tree_dock.h +++ b/editor/scene_tree_dock.h @@ -66,7 +66,7 @@ class SceneTreeDock : public VBoxContainer { TOOL_REPLACE, TOOL_EXTEND_SCRIPT, TOOL_ATTACH_SCRIPT, - TOOL_CLEAR_SCRIPT, + TOOL_DETACH_SCRIPT, TOOL_MOVE_UP, TOOL_MOVE_DOWN, TOOL_DUPLICATE, @@ -110,7 +110,7 @@ class SceneTreeDock : public VBoxContainer { ToolButton *button_add; ToolButton *button_instance; ToolButton *button_create_script; - ToolButton *button_clear_script; + ToolButton *button_detach_script; Button *button_3d; diff --git a/misc/dist/html/full-size.html b/misc/dist/html/full-size.html index 193f2a6aad..c3b67e0c4a 100644 --- a/misc/dist/html/full-size.html +++ b/misc/dist/html/full-size.html @@ -169,8 +169,6 @@ $GODOT_HEAD_INCLUDE var height = window.innerHeight; canvas.width = width * scale; canvas.height = height * scale; - canvas.style.width = width + "px"; - canvas.style.height = height + "px"; } animationCallbacks.push(adjustCanvasDimensions); adjustCanvasDimensions(); diff --git a/modules/mono/build_scripts/godot_tools_build.py b/modules/mono/build_scripts/godot_tools_build.py index 8e77f3bb44..7391e8790d 100644 --- a/modules/mono/build_scripts/godot_tools_build.py +++ b/modules/mono/build_scripts/godot_tools_build.py @@ -15,7 +15,7 @@ def build_godot_tools(source, target, env): from .solution_builder import build_solution - build_solution(env, solution_path, build_config, restore=True) + build_solution(env, solution_path, build_config) # No need to copy targets. The GodotTools csproj takes care of copying them. diff --git a/modules/mono/build_scripts/solution_builder.py b/modules/mono/build_scripts/solution_builder.py index e8ddb7114e..371819fd72 100644 --- a/modules/mono/build_scripts/solution_builder.py +++ b/modules/mono/build_scripts/solution_builder.py @@ -4,7 +4,29 @@ import os verbose = False -def find_msbuild_unix(filename): +def find_dotnet_cli(): + import os.path + + if os.name == "nt": + windows_exts = os.environ["PATHEXT"] + windows_exts = windows_exts.split(os.pathsep) if windows_exts else [] + + for hint_dir in os.environ["PATH"].split(os.pathsep): + hint_dir = hint_dir.strip('"') + hint_path = os.path.join(hint_dir, "dotnet") + if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK): + return hint_path + if os.path.isfile(hint_path + ".exe") and os.access(hint_path + ".exe", os.X_OK): + return hint_path + ".exe" + else: + for hint_dir in os.environ["PATH"].split(os.pathsep): + hint_dir = hint_dir.strip('"') + hint_path = os.path.join(hint_dir, "dotnet") + if os.path.isfile(hint_path) and os.access(hint_path, os.X_OK): + return hint_path + + +def find_msbuild_unix(): import os.path import sys @@ -86,15 +108,7 @@ def run_command(command, args, env_override=None, name=None): raise RuntimeError("'%s' exited with error code: %s" % (name, e.returncode)) -def nuget_restore(env, *args): - global verbose - verbose = env["verbose"] - - # Do NuGet restore - run_command(nuget_path, ["restore"] + list(args), name="nuget restore") - - -def build_solution(env, solution_path, build_config, extra_msbuild_args=[], restore=False): +def build_solution(env, solution_path, build_config, extra_msbuild_args=[]): global verbose verbose = env["verbose"] @@ -104,27 +118,33 @@ def build_solution(env, solution_path, build_config, extra_msbuild_args=[], rest if "PLATFORM" in msbuild_env: del msbuild_env["PLATFORM"] - # Find MSBuild - if os.name == "nt": - msbuild_info = find_msbuild_windows(env) - if msbuild_info is None: - raise RuntimeError("Cannot find MSBuild executable") - msbuild_path = msbuild_info[0] - msbuild_env.update(msbuild_info[1]) + msbuild_args = [] + + dotnet_cli = find_dotnet_cli() + + if dotnet_cli: + msbuild_path = dotnet_cli + msbuild_args += ["msbuild"] # `dotnet msbuild` command else: - msbuild_path = find_msbuild_unix("msbuild") - if msbuild_path is None: - raise RuntimeError("Cannot find MSBuild executable") + # Find MSBuild + if os.name == "nt": + msbuild_info = find_msbuild_windows(env) + if msbuild_info is None: + raise RuntimeError("Cannot find MSBuild executable") + msbuild_path = msbuild_info[0] + msbuild_env.update(msbuild_info[1]) + else: + msbuild_path = find_msbuild_unix() + if msbuild_path is None: + raise RuntimeError("Cannot find MSBuild executable") print("MSBuild path: " + msbuild_path) # Build solution - targets = ["Build"] - if restore: - targets.insert(0, "Restore") + targets = ["Restore", "Build"] - msbuild_args = [solution_path, "/t:%s" % ",".join(targets), "/p:Configuration=" + build_config] + msbuild_args += [solution_path, "/t:%s" % ",".join(targets), "/p:Configuration=" + build_config] msbuild_args += extra_msbuild_args run_command(msbuild_path, msbuild_args, env_override=msbuild_env, name="msbuild") diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp index 6c1c8b87ef..7d00d2d314 100644 --- a/modules/mono/csharp_script.cpp +++ b/modules/mono/csharp_script.cpp @@ -2421,58 +2421,68 @@ void CSharpScript::_update_member_info_no_exports() { bool CSharpScript::_update_exports() { #ifdef TOOLS_ENABLED - if (!Engine::get_singleton()->is_editor_hint()) - return false; - - placeholder_fallback_enabled = true; // until proven otherwise - + bool is_editor = Engine::get_singleton()->is_editor_hint(); + if (is_editor) + placeholder_fallback_enabled = true; // until proven otherwise +#endif if (!valid) return false; bool changed = false; - if (exports_invalidated) { +#ifdef TOOLS_ENABLED + if (exports_invalidated) +#endif + { GD_MONO_SCOPE_THREAD_ATTACH; - exports_invalidated = false; - changed = true; member_info.clear(); - exported_members_cache.clear(); - exported_members_defval_cache.clear(); - // Here we create a temporary managed instance of the class to get the initial values +#ifdef TOOLS_ENABLED + MonoObject *tmp_object = nullptr; + Object *tmp_native = nullptr; + uint32_t tmp_pinned_gchandle = 0; - MonoObject *tmp_object = mono_object_new(mono_domain_get(), script_class->get_mono_ptr()); + if (is_editor) { + exports_invalidated = false; - if (!tmp_object) { - ERR_PRINT("Failed to allocate temporary MonoObject."); - return false; - } + exported_members_cache.clear(); + exported_members_defval_cache.clear(); - uint32_t tmp_pinned_gchandle = GDMonoUtils::new_strong_gchandle_pinned(tmp_object); // pin it (not sure if needed) + // Here we create a temporary managed instance of the class to get the initial values + tmp_object = mono_object_new(mono_domain_get(), script_class->get_mono_ptr()); - GDMonoMethod *ctor = script_class->get_method(CACHED_STRING_NAME(dotctor), 0); + if (!tmp_object) { + ERR_PRINT("Failed to allocate temporary MonoObject."); + return false; + } - ERR_FAIL_NULL_V_MSG(ctor, false, - "Cannot construct temporary MonoObject because the class does not define a parameterless constructor: '" + get_path() + "'."); + tmp_pinned_gchandle = GDMonoUtils::new_strong_gchandle_pinned(tmp_object); // pin it (not sure if needed) - MonoException *ctor_exc = nullptr; - ctor->invoke(tmp_object, nullptr, &ctor_exc); + GDMonoMethod *ctor = script_class->get_method(CACHED_STRING_NAME(dotctor), 0); - Object *tmp_native = GDMonoMarshal::unbox<Object *>(CACHED_FIELD(GodotObject, ptr)->get_value(tmp_object)); + ERR_FAIL_NULL_V_MSG(ctor, false, + "Cannot construct temporary MonoObject because the class does not define a parameterless constructor: '" + get_path() + "'."); - if (ctor_exc) { - // TODO: Should we free 'tmp_native' if the exception was thrown after its creation? + MonoException *ctor_exc = nullptr; + ctor->invoke(tmp_object, nullptr, &ctor_exc); - GDMonoUtils::free_gchandle(tmp_pinned_gchandle); - tmp_object = nullptr; + tmp_native = GDMonoMarshal::unbox<Object *>(CACHED_FIELD(GodotObject, ptr)->get_value(tmp_object)); - ERR_PRINT("Exception thrown from constructor of temporary MonoObject:"); - GDMonoUtils::debug_print_unhandled_exception(ctor_exc); - return false; + if (ctor_exc) { + // TODO: Should we free 'tmp_native' if the exception was thrown after its creation? + + GDMonoUtils::free_gchandle(tmp_pinned_gchandle); + tmp_object = nullptr; + + ERR_PRINT("Exception thrown from constructor of temporary MonoObject:"); + GDMonoUtils::debug_print_unhandled_exception(ctor_exc); + return false; + } } +#endif GDMonoClass *top = script_class; @@ -2488,16 +2498,16 @@ bool CSharpScript::_update_exports() { if (_get_member_export(field, /* inspect export: */ true, prop_info, exported)) { StringName member_name = field->get_name(); - if (exported) { - member_info[member_name] = prop_info; + member_info[member_name] = prop_info; +#ifdef TOOLS_ENABLED + if (is_editor && exported) { exported_members_cache.push_front(prop_info); if (tmp_object) { exported_members_defval_cache[member_name] = GDMonoMarshal::mono_object_to_variant(field->get_value(tmp_object)); } - } else { - member_info[member_name] = prop_info; } +#endif } } @@ -2509,10 +2519,10 @@ bool CSharpScript::_update_exports() { if (_get_member_export(property, /* inspect export: */ true, prop_info, exported)) { StringName member_name = property->get_name(); - if (exported) { - member_info[member_name] = prop_info; + member_info[member_name] = prop_info; +#ifdef TOOLS_ENABLED + if (is_editor && exported) { exported_members_cache.push_front(prop_info); - if (tmp_object) { MonoException *exc = nullptr; MonoObject *ret = property->get_value(tmp_object, &exc); @@ -2523,57 +2533,62 @@ bool CSharpScript::_update_exports() { exported_members_defval_cache[member_name] = GDMonoMarshal::mono_object_to_variant(ret); } } - } else { - member_info[member_name] = prop_info; } +#endif } } top = top->get_parent_class(); } - // Need to check this here, before disposal - bool base_ref = Object::cast_to<Reference>(tmp_native) != nullptr; +#ifdef TOOLS_ENABLED + if (is_editor) { + // Need to check this here, before disposal + bool base_ref = Object::cast_to<Reference>(tmp_native) != nullptr; - // Dispose the temporary managed instance + // Dispose the temporary managed instance - MonoException *exc = nullptr; - GDMonoUtils::dispose(tmp_object, &exc); + MonoException *exc = nullptr; + GDMonoUtils::dispose(tmp_object, &exc); - if (exc) { - ERR_PRINT("Exception thrown from method Dispose() of temporary MonoObject:"); - GDMonoUtils::debug_print_unhandled_exception(exc); - } + if (exc) { + ERR_PRINT("Exception thrown from method Dispose() of temporary MonoObject:"); + GDMonoUtils::debug_print_unhandled_exception(exc); + } - GDMonoUtils::free_gchandle(tmp_pinned_gchandle); - tmp_object = nullptr; + GDMonoUtils::free_gchandle(tmp_pinned_gchandle); + tmp_object = nullptr; - if (tmp_native && !base_ref) { - Node *node = Object::cast_to<Node>(tmp_native); - if (node && node->is_inside_tree()) { - ERR_PRINT("Temporary instance was added to the scene tree."); - } else { - memdelete(tmp_native); + if (tmp_native && !base_ref) { + Node *node = Object::cast_to<Node>(tmp_native); + if (node && node->is_inside_tree()) { + ERR_PRINT("Temporary instance was added to the scene tree."); + } else { + memdelete(tmp_native); + } } } +#endif } - placeholder_fallback_enabled = false; +#ifdef TOOLS_ENABLED + if (is_editor) { + placeholder_fallback_enabled = false; - if (placeholders.size()) { - // Update placeholders if any - Map<StringName, Variant> values; - List<PropertyInfo> propnames; - _update_exports_values(values, propnames); + if (placeholders.size()) { + // Update placeholders if any + Map<StringName, Variant> values; + List<PropertyInfo> propnames; + _update_exports_values(values, propnames); - for (Set<PlaceHolderScriptInstance *>::Element *E = placeholders.front(); E; E = E->next()) { - E->get()->update(propnames, values); + for (Set<PlaceHolderScriptInstance *>::Element *E = placeholders.front(); E; E = E->next()) { + E->get()->update(propnames, values); + } } } +#endif return changed; -#endif - return false; } void CSharpScript::load_script_signals(GDMonoClass *p_class, GDMonoClass *p_native_class) { @@ -2679,7 +2694,6 @@ bool CSharpScript::_get_signal(GDMonoClass *p_class, GDMonoMethod *p_delegate_in return true; } -#ifdef TOOLS_ENABLED /** * Returns false if there was an error, otherwise true. * If there was an error, r_prop_info and r_exported are not assigned any value. @@ -2693,8 +2707,10 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect (m_member->get_enclosing_class()->get_full_name() + "." + (String)m_member->get_name()) if (p_member->is_static()) { +#ifdef TOOLS_ENABLED if (p_member->has_attribute(CACHED_CLASS(ExportAttribute))) ERR_PRINT("Cannot export member because it is static: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); +#endif return false; } @@ -2716,13 +2732,17 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect if (p_member->get_member_type() == IMonoClassMember::MEMBER_TYPE_PROPERTY) { GDMonoProperty *property = static_cast<GDMonoProperty *>(p_member); if (!property->has_getter()) { +#ifdef TOOLS_ENABLED if (exported) ERR_PRINT("Read-only property cannot be exported: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); +#endif return false; } if (!property->has_setter()) { +#ifdef TOOLS_ENABLED if (exported) ERR_PRINT("Write-only property (without getter) cannot be exported: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); +#endif return false; } } @@ -2742,10 +2762,13 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect String hint_string; if (variant_type == Variant::NIL && !nil_is_variant) { +#ifdef TOOLS_ENABLED ERR_PRINT("Unknown exported member type: '" + MEMBER_FULL_QUALIFIED_NAME(p_member) + "'."); +#endif return false; } +#ifdef TOOLS_ENABLED int hint_res = _try_get_member_export_hint(p_member, type, variant_type, /* allow_generics: */ true, hint, hint_string); ERR_FAIL_COND_V_MSG(hint_res == -1, false, @@ -2756,6 +2779,7 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect hint = PropertyHint(CACHED_FIELD(ExportAttribute, hint)->get_int_value(attr)); hint_string = CACHED_FIELD(ExportAttribute, hintString)->get_string_value(attr); } +#endif uint32_t prop_usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE; @@ -2772,6 +2796,7 @@ bool CSharpScript::_get_member_export(IMonoClassMember *p_member, bool p_inspect #undef MEMBER_FULL_QUALIFIED_NAME } +#ifdef TOOLS_ENABLED int CSharpScript::_try_get_member_export_hint(IMonoClassMember *p_member, ManagedType p_type, Variant::Type p_variant_type, bool p_allow_generics, PropertyHint &r_hint, String &r_hint_string) { if (p_variant_type == Variant::NIL) { @@ -3542,10 +3567,15 @@ bool CSharpScript::inherits_script(const Ref<Script> &p_script) const { return false; } -#ifndef _MSC_VER -#warning TODO: Implement CSharpScript::inherits_script and other relevant changes after GH-38063. -#endif - return false; + if (script_class == nullptr || cs->script_class == nullptr) { + return false; + } + + if (script_class == cs->script_class) { + return true; + } + + return cs->script_class->is_assignable_from(script_class); } Ref<Script> CSharpScript::get_base_script() const { diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h index 05e2857538..c9ea482c36 100644 --- a/modules/mono/csharp_script.h +++ b/modules/mono/csharp_script.h @@ -147,8 +147,9 @@ private: bool _get_signal(GDMonoClass *p_class, GDMonoMethod *p_delegate_invoke, Vector<SignalParameter> ¶ms); bool _update_exports(); -#ifdef TOOLS_ENABLED + bool _get_member_export(IMonoClassMember *p_member, bool p_inspect_export, PropertyInfo &r_prop_info, bool &r_exported); +#ifdef TOOLS_ENABLED static int _try_get_member_export_hint(IMonoClassMember *p_member, ManagedType p_type, Variant::Type p_variant_type, bool p_allow_generics, PropertyHint &r_hint, String &r_hint_string); #endif diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj index 3bb136e374..9cb50014b0 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/GodotTools.ProjectEditor.csproj @@ -6,18 +6,18 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Build" Version="16.5.0" /> - <PackageReference Include="Microsoft.Build.Runtime" Version="16.5.0" /> + <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\GodotTools.Core\GodotTools.Core.csproj" /> </ItemGroup> - <PropertyGroup> + <ItemGroup> <!-- - The 'Microsoft.Build.Runtime' package includes an mscorlib reference assembly in contentFiles. - This causes our project build to fail. As a workaround, we remove {CandidateAssemblyFiles} - from AssemblySearchPaths as described here: https://github.com/microsoft/msbuild/issues/3486. + The Microsoft.Build.Runtime package is too problematic so we create a MSBuild.exe stub. The workaround described + here doesn't work with Microsoft.NETFramework.ReferenceAssemblies: https://github.com/microsoft/msbuild/issues/3486 + We need a MSBuild.exe file as there's an issue in Microsoft.Build where it executes platform dependent code when + searching for MSBuild.exe before the fallback to not using it. A stub is fine as it should never be executed. --> - <AssemblySearchPaths>$([System.String]::Copy('$(AssemblySearchPaths)').Replace('{CandidateAssemblyFiles}', ''))</AssemblySearchPaths> - <AssemblySearchPaths Condition=" '$(MSBuildRuntimeVersion)' != '' ">$(AssemblySearchPaths.Split(';'))</AssemblySearchPaths> - </PropertyGroup> + <None Include="MSBuild.exe" CopyToOutputDirectory="Always" /> + </ItemGroup> </Project> diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/MSBuild.exe diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs index cbe3afaedd..fb2beb6995 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectGenerator.cs @@ -125,6 +125,12 @@ namespace GodotTools.ProjectEditor // References var referenceGroup = root.AddItemGroup(); referenceGroup.AddItem("Reference", "System"); + var frameworkRefAssembliesItem = referenceGroup.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies"); + + // Use metadata (child nodes) instead of attributes for the PackageReference. + // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build. + frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0"); + frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All"); root.AddImport(Path.Combine("$(MSBuildBinPath)", "Microsoft.CSharp.targets").Replace("/", "\\")); diff --git a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs index a0356d0f49..069a1edaa3 100644 --- a/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs +++ b/modules/mono/editor/GodotTools/GodotTools.ProjectEditor/ProjectUtils.cs @@ -173,7 +173,7 @@ namespace GodotTools.ProjectEditor void AddPropertyIfNotPresent(string name, string condition, string value) { if (root.PropertyGroups - .Any(g => (g.Condition == string.Empty || g.Condition.Trim() == condition) && + .Any(g => (string.IsNullOrEmpty(g.Condition) || g.Condition.Trim() == condition) && g.Properties .Any(p => p.Name == name && p.Value == value && @@ -264,7 +264,7 @@ namespace GodotTools.ProjectEditor bool hasGodotProjectGeneratorVersion = false; bool foundOldConfiguration = false; - foreach (var propertyGroup in root.PropertyGroups.Where(g => g.Condition == string.Empty)) + foreach (var propertyGroup in root.PropertyGroups.Where(g => string.IsNullOrEmpty(g.Condition))) { if (!hasGodotProjectGeneratorVersion && propertyGroup.Properties.Any(p => p.Name == "GodotProjectGeneratorVersion")) hasGodotProjectGeneratorVersion = true; @@ -280,7 +280,7 @@ namespace GodotTools.ProjectEditor if (!hasGodotProjectGeneratorVersion) { - root.PropertyGroups.First(g => g.Condition == string.Empty)? + root.PropertyGroups.First(g => string.IsNullOrEmpty(g.Condition))? .AddProperty("GodotProjectGeneratorVersion", Assembly.GetExecutingAssembly().GetName().Version.ToString()); project.HasUnsavedChanges = true; } @@ -348,5 +348,25 @@ namespace GodotTools.ProjectEditor MigrateConfigurationConditions("Tools", "Debug"); // Must be last } } + + public static void EnsureHasNugetNetFrameworkRefAssemblies(MSBuildProject project) + { + var root = project.Root; + + bool found = root.ItemGroups.Any(g => string.IsNullOrEmpty(g.Condition) && g.Items.Any( + item => item.ItemType == "PackageReference" && item.Include == "Microsoft.NETFramework.ReferenceAssemblies")); + + if (found) + return; + + var frameworkRefAssembliesItem = root.AddItem("PackageReference", "Microsoft.NETFramework.ReferenceAssemblies"); + + // Use metadata (child nodes) instead of attributes for the PackageReference. + // This is for compatibility with 3.2, where GodotTools uses an old Microsoft.Build. + frameworkRefAssembliesItem.AddMetadata("Version", "1.0.0"); + frameworkRefAssembliesItem.AddMetadata("PrivateAssets", "All"); + + project.HasUnsavedChanges = true; + } } } diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs index f4cda3e522..e55558c100 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildSystem.cs @@ -14,16 +14,6 @@ namespace GodotTools.Build { public static class BuildSystem { - private static string GetMsBuildPath() - { - string msbuildPath = MsBuildFinder.FindMsBuild(); - - if (msbuildPath == null) - throw new FileNotFoundException("Cannot find the MSBuild executable."); - - return msbuildPath; - } - private static string MonoWindowsBinDir { get @@ -46,8 +36,8 @@ namespace GodotTools.Build { if (OS.IsWindows) { - return (BuildManager.BuildTool)EditorSettings.GetSetting("mono/builds/build_tool") - == BuildManager.BuildTool.MsBuildMono; + return (BuildTool)EditorSettings.GetSetting("mono/builds/build_tool") + == BuildTool.MsBuildMono; } return false; @@ -57,16 +47,21 @@ namespace GodotTools.Build private static bool PrintBuildOutput => (bool)EditorSettings.GetSetting("mono/builds/print_build_output"); - private static Process LaunchBuild(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) + private static Process LaunchBuild(string solution, IEnumerable<string> targets, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) { + (string msbuildPath, BuildTool buildTool) = MsBuildFinder.FindMsBuild(); + + if (msbuildPath == null) + throw new FileNotFoundException("Cannot find the MSBuild executable."); + var customPropertiesList = new List<string>(); if (customProperties != null) customPropertiesList.AddRange(customProperties); - string compilerArgs = BuildArguments(solution, config, loggerOutputDir, customPropertiesList); + string compilerArgs = BuildArguments(buildTool, solution, targets, config, loggerOutputDir, customPropertiesList); - var startInfo = new ProcessStartInfo(GetMsBuildPath(), compilerArgs); + var startInfo = new ProcessStartInfo(msbuildPath, compilerArgs); bool redirectOutput = !IsDebugMsBuildRequested() && !PrintBuildOutput; @@ -90,7 +85,7 @@ namespace GodotTools.Build // Needed when running from Developer Command Prompt for VS RemovePlatformVariable(startInfo.EnvironmentVariables); - var process = new Process { StartInfo = startInfo }; + var process = new Process {StartInfo = startInfo}; process.Start(); @@ -105,19 +100,19 @@ namespace GodotTools.Build public static int Build(BuildInfo buildInfo) { - return Build(buildInfo.Solution, buildInfo.Configuration, + return Build(buildInfo.Solution, buildInfo.Targets, buildInfo.Configuration, buildInfo.LogsDirPath, buildInfo.CustomProperties); } public static Task<int> BuildAsync(BuildInfo buildInfo) { - return BuildAsync(buildInfo.Solution, buildInfo.Configuration, + return BuildAsync(buildInfo.Solution, buildInfo.Targets, buildInfo.Configuration, buildInfo.LogsDirPath, buildInfo.CustomProperties); } - public static int Build(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) + public static int Build(string solution, string[] targets, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) { - using (var process = LaunchBuild(solution, config, loggerOutputDir, customProperties)) + using (var process = LaunchBuild(solution, targets, config, loggerOutputDir, customProperties)) { process.WaitForExit(); @@ -125,9 +120,9 @@ namespace GodotTools.Build } } - public static async Task<int> BuildAsync(string solution, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) + public static async Task<int> BuildAsync(string solution, IEnumerable<string> targets, string config, string loggerOutputDir, IEnumerable<string> customProperties = null) { - using (var process = LaunchBuild(solution, config, loggerOutputDir, customProperties)) + using (var process = LaunchBuild(solution, targets, config, loggerOutputDir, customProperties)) { await process.WaitForExitAsync(); @@ -135,10 +130,15 @@ namespace GodotTools.Build } } - private static string BuildArguments(string solution, string config, string loggerOutputDir, List<string> customProperties) + private static string BuildArguments(BuildTool buildTool, string solution, IEnumerable<string> targets, string config, string loggerOutputDir, IEnumerable<string> customProperties) { - string arguments = $@"""{solution}"" /v:normal /t:Build ""/p:{"Configuration=" + config}"" " + - $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{loggerOutputDir}"""; + string arguments = string.Empty; + + if (buildTool == BuildTool.DotnetCli) + arguments += "msbuild "; // `dotnet msbuild` command + + arguments += $@"""{solution}"" /v:normal /t:{string.Join(",", targets)} ""/p:{"Configuration=" + config}"" " + + $@"""/l:{typeof(GodotBuildLogger).FullName},{GodotBuildLogger.AssemblyPath};{loggerOutputDir}"""; foreach (string customProperty in customProperties) { diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs new file mode 100644 index 0000000000..a1a69334e3 --- /dev/null +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildTool.cs @@ -0,0 +1,10 @@ +namespace GodotTools.Build +{ + public enum BuildTool + { + MsBuildMono, + MsBuildVs, + JetBrainsMsBuild, + DotnetCli + } +} diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs index 73de09c116..f36e581a5f 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/MsBuildFinder.cs @@ -17,16 +17,24 @@ namespace GodotTools.Build private static string _msbuildToolsPath = string.Empty; private static string _msbuildUnixPath = string.Empty; - public static string FindMsBuild() + public static (string, BuildTool) FindMsBuild() { var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var buildTool = (BuildManager.BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); + var buildTool = (BuildTool)editorSettings.GetSetting("mono/builds/build_tool"); if (OS.IsWindows) { switch (buildTool) { - case BuildManager.BuildTool.MsBuildVs: + case BuildTool.DotnetCli: + { + string dotnetCliPath = OS.PathWhich("dotnet"); + if (!string.IsNullOrEmpty(dotnetCliPath)) + return (dotnetCliPath, BuildTool.DotnetCli); + GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Visual Studio."); + goto case BuildTool.MsBuildVs; + } + case BuildTool.MsBuildVs: { if (string.IsNullOrEmpty(_msbuildToolsPath) || !File.Exists(_msbuildToolsPath)) { @@ -40,18 +48,18 @@ namespace GodotTools.Build if (!_msbuildToolsPath.EndsWith("\\")) _msbuildToolsPath += "\\"; - return Path.Combine(_msbuildToolsPath, "MSBuild.exe"); + return (Path.Combine(_msbuildToolsPath, "MSBuild.exe"), BuildTool.MsBuildVs); } - case BuildManager.BuildTool.MsBuildMono: + case BuildTool.MsBuildMono: { string msbuildPath = Path.Combine(Internal.MonoWindowsInstallRoot, "bin", "msbuild.bat"); if (!File.Exists(msbuildPath)) throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildMono}'. Tried with path: {msbuildPath}"); - return msbuildPath; + return (msbuildPath, BuildTool.MsBuildMono); } - case BuildManager.BuildTool.JetBrainsMsBuild: + case BuildTool.JetBrainsMsBuild: { var editorPath = (string)editorSettings.GetSetting(RiderPathManager.EditorPathSettingName); @@ -65,7 +73,7 @@ namespace GodotTools.Build if (!File.Exists(msbuildPath)) throw new FileNotFoundException($"Cannot find executable for '{BuildManager.PropNameMSBuildJetBrains}'. Tried with path: {msbuildPath}"); - return msbuildPath; + return (msbuildPath, BuildTool.JetBrainsMsBuild); } default: throw new IndexOutOfRangeException("Invalid build tool in editor settings"); @@ -74,21 +82,32 @@ namespace GodotTools.Build if (OS.IsUnixLike) { - if (buildTool == BuildManager.BuildTool.MsBuildMono) + switch (buildTool) { - if (string.IsNullOrEmpty(_msbuildUnixPath) || !File.Exists(_msbuildUnixPath)) + case BuildTool.DotnetCli: { - // Try to search it again if it wasn't found last time or if it was removed from its location - _msbuildUnixPath = FindBuildEngineOnUnix("msbuild"); + string dotnetCliPath = OS.PathWhich("dotnet"); + if (!string.IsNullOrEmpty(dotnetCliPath)) + return (dotnetCliPath, BuildTool.DotnetCli); + GD.PushError("Cannot find dotnet CLI executable. Fallback to MSBuild from Mono."); + goto case BuildTool.MsBuildMono; } + case BuildTool.MsBuildMono: + { + if (string.IsNullOrEmpty(_msbuildUnixPath) || !File.Exists(_msbuildUnixPath)) + { + // Try to search it again if it wasn't found last time or if it was removed from its location + _msbuildUnixPath = FindBuildEngineOnUnix("msbuild"); + } - if (string.IsNullOrEmpty(_msbuildUnixPath)) - throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'"); + if (string.IsNullOrEmpty(_msbuildUnixPath)) + throw new FileNotFoundException($"Cannot find binary for '{BuildManager.PropNameMSBuildMono}'"); - return _msbuildUnixPath; + return (_msbuildUnixPath, BuildTool.MsBuildMono); + } + default: + throw new IndexOutOfRangeException("Invalid build tool in editor settings"); } - - throw new IndexOutOfRangeException("Invalid build tool in editor settings"); } throw new PlatformNotSupportedException(); diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs index 70bd552f2f..cca0983c01 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildInfo.cs @@ -10,6 +10,7 @@ namespace GodotTools public sealed class BuildInfo : Reference // TODO Remove Reference once we have proper serialization { public string Solution { get; } + public string[] Targets { get; } public string Configuration { get; } public Array<string> CustomProperties { get; } = new Array<string>(); // TODO Use List once we have proper serialization @@ -38,9 +39,10 @@ namespace GodotTools { } - public BuildInfo(string solution, string configuration) + public BuildInfo(string solution, string[] targets, string configuration) { Solution = solution; + Targets = targets; Configuration = configuration; } } diff --git a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs index 2d1b873808..598787ba03 100644 --- a/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/BuildManager.cs @@ -18,17 +18,11 @@ namespace GodotTools public const string PropNameMSBuildMono = "MSBuild (Mono)"; public const string PropNameMSBuildVs = "MSBuild (VS Build Tools)"; public const string PropNameMSBuildJetBrains = "MSBuild (JetBrains Rider)"; + public const string PropNameDotnetCli = "dotnet CLI"; public const string MsBuildIssuesFileName = "msbuild_issues.csv"; public const string MsBuildLogFileName = "msbuild_log.txt"; - public enum BuildTool - { - MsBuildMono, - MsBuildVs, - JetBrainsMsBuild - } - private static void RemoveOldIssuesFile(BuildInfo buildInfo) { var issuesFile = GetIssuesFilePath(buildInfo); @@ -181,10 +175,12 @@ namespace GodotTools { pr.Step("Building project solution", 0); - var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, config); + var buildInfo = new BuildInfo(GodotSharpDirs.ProjectSlnPath, targets: new[] {"Restore", "Build"}, config); + + bool escapeNeedsDoubleBackslash = buildTool == BuildTool.MsBuildMono || buildTool == BuildTool.DotnetCli; // Add Godot defines - string constants = buildTool != BuildTool.MsBuildMono ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; + string constants = !escapeNeedsDoubleBackslash ? "GodotDefineConstants=\"" : "GodotDefineConstants=\\\""; foreach (var godotDefine in godotDefines) constants += $"GODOT_{godotDefine.ToUpper().Replace("-", "_").Replace(" ", "_").Replace(";", "_")};"; @@ -192,7 +188,7 @@ namespace GodotTools if (Internal.GodotIsRealTDouble()) constants += "GODOT_REAL_T_IS_DOUBLE;"; - constants += buildTool != BuildTool.MsBuildMono ? "\"" : "\\\""; + constants += !escapeNeedsDoubleBackslash ? "\"" : "\\\""; buildInfo.CustomProperties.Add(constants); @@ -250,20 +246,44 @@ namespace GodotTools { // Build tool settings var editorSettings = GodotSharpEditor.Instance.GetEditorInterface().GetEditorSettings(); - var msbuild = BuildTool.MsBuildMono; + + BuildTool msbuildDefault; + if (OS.IsWindows) - msbuild = RiderPathManager.IsExternalEditorSetToRider(editorSettings) ? BuildTool.JetBrainsMsBuild : BuildTool.MsBuildVs; + { + if (RiderPathManager.IsExternalEditorSetToRider(editorSettings)) + msbuildDefault = BuildTool.JetBrainsMsBuild; + else + msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildVs; + } + else + { + msbuildDefault = !string.IsNullOrEmpty(OS.PathWhich("dotnet")) ? BuildTool.DotnetCli : BuildTool.MsBuildMono; + } + + EditorDef("mono/builds/build_tool", msbuildDefault); - EditorDef("mono/builds/build_tool", msbuild); + string hintString; + + if (OS.IsWindows) + { + hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," + + $"{PropNameMSBuildVs}:{(int)BuildTool.MsBuildVs}," + + $"{PropNameMSBuildJetBrains}:{(int)BuildTool.JetBrainsMsBuild}," + + $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}"; + } + else + { + hintString = $"{PropNameMSBuildMono}:{(int)BuildTool.MsBuildMono}," + + $"{PropNameDotnetCli}:{(int)BuildTool.DotnetCli}"; + } editorSettings.AddPropertyInfo(new Godot.Collections.Dictionary { ["type"] = Godot.Variant.Type.Int, ["name"] = "mono/builds/build_tool", ["hint"] = Godot.PropertyHint.Enum, - ["hint_string"] = OS.IsWindows ? - $"{PropNameMSBuildMono},{PropNameMSBuildVs},{PropNameMSBuildJetBrains}" : - $"{PropNameMSBuildMono}" + ["hint_string"] = hintString }); EditorDef("mono/builds/print_build_output", false); diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs index ec3cae00d5..eb7696685f 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs +++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs @@ -461,6 +461,9 @@ namespace GodotTools // Make sure the existing project has Api assembly references configured correctly ProjectUtils.FixApiHintPath(msbuildProject); + // Make sure the existing project references the Microsoft.NETFramework.ReferenceAssemblies nuget package + ProjectUtils.EnsureHasNugetNetFrameworkRefAssemblies(msbuildProject); + if (msbuildProject.HasUnsavedChanges) { // Save a copy of the project before replacing it diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj index 6857663a4c..ba527ca3b5 100644 --- a/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj +++ b/modules/mono/editor/GodotTools/GodotTools/GodotTools.csproj @@ -17,6 +17,7 @@ </PropertyGroup> <ItemGroup> <PackageReference Include="JetBrains.Annotations" Version="2019.1.3.0" ExcludeAssets="runtime" PrivateAssets="all" /> + <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <Reference Include="GodotSharp"> <HintPath>$(GodotApiAssembliesDir)/GodotSharp.dll</HintPath> diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj index ba0bbd7630..b5ac124c9a 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj @@ -30,6 +30,7 @@ <ConsolePause>false</ConsolePause> </PropertyGroup> <ItemGroup> + <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> <Reference Include="System" /> </ItemGroup> <ItemGroup> diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj index 22853797c1..8785931312 100644 --- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj +++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj @@ -30,6 +30,7 @@ <ConsolePause>false</ConsolePause> </PropertyGroup> <ItemGroup> + <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" /> <Reference Include="System" /> </ItemGroup> <ItemGroup> diff --git a/modules/webrtc/webrtc_data_channel_js.cpp b/modules/webrtc/webrtc_data_channel_js.cpp index 1b360720a2..40c3f5801b 100644 --- a/modules/webrtc/webrtc_data_channel_js.cpp +++ b/modules/webrtc/webrtc_data_channel_js.cpp @@ -312,14 +312,14 @@ WebRTCDataChannelJS::WebRTCDataChannelJS(int js_id) { return; } var len = buffer.length*buffer.BYTES_PER_ELEMENT; - var out = Module._malloc(len); - Module.HEAPU8.set(buffer, out); + var out = _malloc(len); + HEAPU8.set(buffer, out); ccall("_emrtc_on_ch_message", "void", ["number", "number", "number", "number"], [c_ptr, out, len, is_string] ); - Module._free(out); + _free(out); } }, this, js_id); diff --git a/modules/websocket/emws_client.cpp b/modules/websocket/emws_client.cpp index bbe4d6dc5b..bceb65c315 100644 --- a/modules/websocket/emws_client.cpp +++ b/modules/websocket/emws_client.cpp @@ -142,14 +142,14 @@ Error EMWSClient::connect_to_host(String p_host, String p_path, uint16_t p_port, } var len = buffer.length*buffer.BYTES_PER_ELEMENT; - var out = Module._malloc(len); - Module.HEAPU8.set(buffer, out); + var out = _malloc(len); + HEAPU8.set(buffer, out); ccall("_esws_on_message", "void", ["number", "number", "number", "number"], [c_ptr, out, len, is_string] ); - Module._free(out); + _free(out); }); socket.addEventListener("error", function (event) { diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 7239648937..dcf9a46bf9 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -4,6 +4,7 @@ Import("env") javascript_files = [ "audio_driver_javascript.cpp", + "display_server_javascript.cpp", "http_client_javascript.cpp", "javascript_eval.cpp", "javascript_main.cpp", @@ -17,22 +18,22 @@ if env["threads_enabled"]: build = env.add_program(build_targets, javascript_files) js_libraries = [ - "http_request.js", + "native/http_request.js", ] for lib in js_libraries: env.Append(LINKFLAGS=["--js-library", env.File(lib).path]) env.Depends(build, js_libraries) -js_modules = [ - "id_handler.js", +js_pre = [ + "native/id_handler.js", + "native/utils.js", ] -for module in js_modules: - env.Append(LINKFLAGS=["--pre-js", env.File(module).path]) -env.Depends(build, js_modules) +for js in js_pre: + env.Append(LINKFLAGS=["--pre-js", env.File(js).path]) +env.Depends(build, js_pre) engine = [ "engine/preloader.js", - "engine/loader.js", "engine/utils.js", "engine/engine.js", ] @@ -47,11 +48,17 @@ wrap_list = [ js_wrapped = env.Textfile("#bin/godot", [env.File(f) for f in wrap_list], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js") zip_dir = env.Dir("#bin/.javascript_zip") -out_files = [zip_dir.File("godot.js"), zip_dir.File("godot.wasm"), zip_dir.File("godot.html")] -in_files = [js_wrapped, build[1], "#misc/dist/html/full-size.html"] +binary_name = "godot.tools" if env["tools"] else "godot" +out_files = [ + zip_dir.File(binary_name + ".js"), + zip_dir.File(binary_name + ".wasm"), + zip_dir.File(binary_name + ".html"), +] +html_file = "#misc/dist/html/full-size.html" +in_files = [js_wrapped, build[1], html_file] if env["threads_enabled"]: in_files.append(build[2]) - out_files.append(zip_dir.File("godot.worker.js")) + out_files.append(zip_dir.File(binary_name + ".worker.js")) zip_files = env.InstallAs(out_files, in_files) env.Zip( diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 8f857478e5..690ce5ba47 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -190,19 +190,43 @@ void AudioDriverJavaScript::lock() { void AudioDriverJavaScript::unlock() { } -void AudioDriverJavaScript::finish() { +void AudioDriverJavaScript::finish_async() { + + // Close the context, add the operation to the async_finish list in module. + int id = _driver_id; + _driver_id = 0; /* clang-format off */ EM_ASM({ + var ref = Module.IDHandler.get($0); + Module.async_finish.push(new Promise(function(accept, reject) { + if (!ref) { + console.log("Ref not found!", $0, Module.IDHandler); + setTimeout(accept, 0); + } else { + const context = ref['context']; + // Disconnect script and input. + ref['script'].disconnect(); + if (ref['input']) + ref['input'].disconnect(); + ref = null; + context.close().then(function() { + accept(); + }).catch(function(e) { + accept(); + }); + } + })); Module.IDHandler.remove($0); - }, _driver_id); + }, id); /* clang-format on */ +} +void AudioDriverJavaScript::finish() { if (internal_buffer) { memdelete_arr(internal_buffer); internal_buffer = nullptr; } - _driver_id = 0; } Error AudioDriverJavaScript::capture_start() { diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index f6f2dacd4e..4a44f4683f 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -56,6 +56,7 @@ public: virtual void lock(); virtual void unlock(); virtual void finish(); + void finish_async(); virtual Error capture_start(); virtual Error capture_stop(); diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 9486e10717..81287cead8 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -22,7 +22,7 @@ def get_opts(): # eval() can be a security concern, so it can be disabled. BoolVariable("javascript_eval", "Enable JavaScript eval interface", True), BoolVariable("threads_enabled", "Enable WebAssembly Threads support (limited browser support)", False), - BoolVariable("use_closure_compiler", "Use closure compiler to minimize Javascript code", False), + BoolVariable("use_closure_compiler", "Use closure compiler to minimize JavaScript code", False), ] @@ -57,7 +57,7 @@ def configure(env): env.Append(CPPDEFINES=["DEBUG_ENABLED"]) # Retain function names for backtraces at the cost of file size. env.Append(LINKFLAGS=["--profiling-funcs"]) - else: # 'debug' + else: # "debug" env.Append(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(CCFLAGS=["-O1", "-g"]) env.Append(LINKFLAGS=["-O1", "-g"]) @@ -150,7 +150,7 @@ def configure(env): env.Append(LIBS=["idbfs.js"]) env.Append(LINKFLAGS=["-s", "BINARYEN=1"]) - env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", 'EXPORT_NAME="Godot"']) + env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", "EXPORT_NAME='Godot'"]) # Allow increasing memory buffer size during runtime. This is efficient # when using WebAssembly (in comparison to asm.js) and works well for @@ -162,5 +162,10 @@ def configure(env): env.Append(LINKFLAGS=["-s", "INVOKE_RUN=0"]) - # callMain for manual start, FS for preloading. - env.Append(LINKFLAGS=["-s", 'EXTRA_EXPORTED_RUNTIME_METHODS=["callMain", "FS"]']) + # Allow use to take control of swapping WebGL buffers. + env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"]) + + # callMain for manual start, FS for preloading, PATH and ERRNO_CODES for BrowserFS. + env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain', 'FS', 'PATH']"]) + # Add code that allow exiting runtime. + env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"]) diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp new file mode 100644 index 0000000000..060e446fca --- /dev/null +++ b/platform/javascript/display_server_javascript.cpp @@ -0,0 +1,1208 @@ +#include "platform/javascript/display_server_javascript.h" + +#include "drivers/dummy/rasterizer_dummy.h" +#include "platform/javascript/os_javascript.h" + +#include <emscripten.h> +#include <png.h> + +#include "dom_keys.inc" + +#define DOM_BUTTON_LEFT 0 +#define DOM_BUTTON_MIDDLE 1 +#define DOM_BUTTON_RIGHT 2 +#define DOM_BUTTON_XBUTTON1 3 +#define DOM_BUTTON_XBUTTON2 4 + +DisplayServerJavaScript *DisplayServerJavaScript::get_singleton() { + return static_cast<DisplayServerJavaScript *>(DisplayServer::get_singleton()); +} + +// Window (canvas) +extern "C" EMSCRIPTEN_KEEPALIVE void _set_canvas_id(uint8_t *p_data, int p_data_size) { + DisplayServerJavaScript *display = DisplayServerJavaScript::get_singleton(); + display->canvas_id.parse_utf8((const char *)p_data, p_data_size); + display->canvas_id = "#" + display->canvas_id; +} + +static void focus_canvas() { + + /* clang-format off */ + EM_ASM( + Module['canvas'].focus(); + ); + /* clang-format on */ +} + +static bool is_canvas_focused() { + + /* clang-format off */ + return EM_ASM_INT_V( + return document.activeElement == Module['canvas']; + ); + /* clang-format on */ +} + +static Point2 compute_position_in_canvas(int x, int y) { + DisplayServerJavaScript *display = DisplayServerJavaScript::get_singleton(); + int canvas_x = EM_ASM_INT({ + return Module['canvas'].getBoundingClientRect().x; + }); + int canvas_y = EM_ASM_INT({ + return Module['canvas'].getBoundingClientRect().y; + }); + int canvas_width; + int canvas_height; + emscripten_get_canvas_element_size(display->canvas_id.utf8().get_data(), &canvas_width, &canvas_height); + + double element_width; + double element_height; + emscripten_get_element_css_size(display->canvas_id.utf8().get_data(), &element_width, &element_height); + + return Point2((int)(canvas_width / element_width * (x - canvas_x)), + (int)(canvas_height / element_height * (y - canvas_y))); +} + +static bool cursor_inside_canvas = true; + +EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) { + + DisplayServerJavaScript *display = get_singleton(); + // Empty ID is canvas. + String target_id = String::utf8(p_event->id); + if (target_id.empty() || "#" + target_id == display->canvas_id) { + // This event property is the only reliable data on + // browser fullscreen state. + if (p_event->isFullscreen) { + display->window_mode = WINDOW_MODE_FULLSCREEN; + } else { + display->window_mode = WINDOW_MODE_WINDOWED; + } + } + return false; +} + +// Drag and drop callback (see native/utils.js). +extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p_filec) { + DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); + if (!ds) { + ERR_FAIL_MSG("Unable to drop files because the DisplayServer is not active"); + } + if (ds->drop_files_callback.is_null()) + return; + Vector<String> files; + for (int i = 0; i < p_filec; i++) { + files.push_back(String::utf8(p_filev[i])); + } + Variant v = files; + Variant *vp = &v; + Variant ret; + Callable::CallError ce; + ds->drop_files_callback.call((const Variant **)&vp, 1, ret, ce); +} + +// Keys + +template <typename T> +static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event) { + + godot_event->set_shift(emscripten_event_ptr->shiftKey); + godot_event->set_alt(emscripten_event_ptr->altKey); + godot_event->set_control(emscripten_event_ptr->ctrlKey); + godot_event->set_metakey(emscripten_event_ptr->metaKey); +} + +static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) { + + Ref<InputEventKey> ev; + ev.instance(); + ev->set_echo(emscripten_event->repeat); + dom2godot_mod(emscripten_event, ev); + ev->set_keycode(dom2godot_keycode(emscripten_event->keyCode)); + ev->set_physical_keycode(dom2godot_keycode(emscripten_event->keyCode)); + + String unicode = String::utf8(emscripten_event->key); + // Check if empty or multi-character (e.g. `CapsLock`). + if (unicode.length() != 1) { + // Might be empty as well, but better than nonsense. + unicode = String::utf8(emscripten_event->charValue); + } + if (unicode.length() == 1) { + ev->set_unicode(unicode[0]); + } + + return ev; +} + +EM_BOOL DisplayServerJavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { + + DisplayServerJavaScript *display = get_singleton(); + Ref<InputEventKey> ev = setup_key_event(p_event); + ev->set_pressed(true); + if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_keycode())) { + // Defer to keypress event for legacy unicode retrieval. + display->deferred_key_event = ev; + // Do not suppress keypress event. + return false; + } + Input::get_singleton()->parse_input_event(ev); + return true; +} + +EM_BOOL DisplayServerJavaScript::keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { + + DisplayServerJavaScript *display = get_singleton(); + display->deferred_key_event->set_unicode(p_event->charCode); + Input::get_singleton()->parse_input_event(display->deferred_key_event); + return true; +} + +EM_BOOL DisplayServerJavaScript::keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { + + Ref<InputEventKey> ev = setup_key_event(p_event); + ev->set_pressed(false); + Input::get_singleton()->parse_input_event(ev); + return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != 0; +} + +// Mouse + +EM_BOOL DisplayServerJavaScript::mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { + + DisplayServerJavaScript *display = get_singleton(); + + Ref<InputEventMouseButton> ev; + ev.instance(); + ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_MOUSEDOWN); + ev->set_position(compute_position_in_canvas(p_event->clientX, p_event->clientY)); + ev->set_global_position(ev->get_position()); + dom2godot_mod(p_event, ev); + + switch (p_event->button) { + case DOM_BUTTON_LEFT: + ev->set_button_index(BUTTON_LEFT); + break; + case DOM_BUTTON_MIDDLE: + ev->set_button_index(BUTTON_MIDDLE); + break; + case DOM_BUTTON_RIGHT: + ev->set_button_index(BUTTON_RIGHT); + break; + case DOM_BUTTON_XBUTTON1: + ev->set_button_index(BUTTON_XBUTTON1); + break; + case DOM_BUTTON_XBUTTON2: + ev->set_button_index(BUTTON_XBUTTON2); + break; + default: + return false; + } + + if (ev->is_pressed()) { + + double diff = emscripten_get_now() - display->last_click_ms; + + if (ev->get_button_index() == display->last_click_button_index) { + + if (diff < 400 && Point2(display->last_click_pos).distance_to(ev->get_position()) < 5) { + + display->last_click_ms = 0; + display->last_click_pos = Point2(-100, -100); + display->last_click_button_index = -1; + ev->set_doubleclick(true); + } + + } else { + display->last_click_button_index = ev->get_button_index(); + } + + if (!ev->is_doubleclick()) { + display->last_click_ms += diff; + display->last_click_pos = ev->get_position(); + } + } + + Input *input = Input::get_singleton(); + int mask = input->get_mouse_button_mask(); + int button_flag = 1 << (ev->get_button_index() - 1); + if (ev->is_pressed()) { + // Since the event is consumed, focus manually. The containing iframe, + // if exists, may not have focus yet, so focus even if already focused. + focus_canvas(); + mask |= button_flag; + } else if (mask & button_flag) { + mask &= ~button_flag; + } else { + // Received release event, but press was outside the canvas, so ignore. + return false; + } + ev->set_button_mask(mask); + + input->parse_input_event(ev); + // Prevent multi-click text selection and wheel-click scrolling anchor. + // Context menu is prevented through contextmenu event. + return true; +} + +EM_BOOL DisplayServerJavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { + + Input *input = Input::get_singleton(); + int input_mask = input->get_mouse_button_mask(); + Point2 pos = compute_position_in_canvas(p_event->clientX, p_event->clientY); + // For motion outside the canvas, only read mouse movement if dragging + // started inside the canvas; imitating desktop app behaviour. + if (!cursor_inside_canvas && !input_mask) + return false; + + Ref<InputEventMouseMotion> ev; + ev.instance(); + dom2godot_mod(p_event, ev); + ev->set_button_mask(input_mask); + + ev->set_position(pos); + ev->set_global_position(ev->get_position()); + + ev->set_relative(Vector2(p_event->movementX, p_event->movementY)); + input->set_mouse_position(ev->get_position()); + ev->set_speed(input->get_last_mouse_speed()); + + input->parse_input_event(ev); + // Don't suppress mouseover/-leave events. + return false; +} + +// Cursor +static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape) { + + switch (p_shape) { + case DisplayServer::CURSOR_ARROW: + return "auto"; + case DisplayServer::CURSOR_IBEAM: + return "text"; + case DisplayServer::CURSOR_POINTING_HAND: + return "pointer"; + case DisplayServer::CURSOR_CROSS: + return "crosshair"; + case DisplayServer::CURSOR_WAIT: + return "progress"; + case DisplayServer::CURSOR_BUSY: + return "wait"; + case DisplayServer::CURSOR_DRAG: + return "grab"; + case DisplayServer::CURSOR_CAN_DROP: + return "grabbing"; + case DisplayServer::CURSOR_FORBIDDEN: + return "no-drop"; + case DisplayServer::CURSOR_VSIZE: + return "ns-resize"; + case DisplayServer::CURSOR_HSIZE: + return "ew-resize"; + case DisplayServer::CURSOR_BDIAGSIZE: + return "nesw-resize"; + case DisplayServer::CURSOR_FDIAGSIZE: + return "nwse-resize"; + case DisplayServer::CURSOR_MOVE: + return "move"; + case DisplayServer::CURSOR_VSPLIT: + return "row-resize"; + case DisplayServer::CURSOR_HSPLIT: + return "col-resize"; + case DisplayServer::CURSOR_HELP: + return "help"; + default: + return "auto"; + } +} + +static void set_css_cursor(const char *p_cursor) { + + /* clang-format off */ + EM_ASM_({ + Module['canvas'].style.cursor = UTF8ToString($0); + }, p_cursor); + /* clang-format on */ +} + +static bool is_css_cursor_hidden() { + + /* clang-format off */ + return EM_ASM_INT({ + return Module['canvas'].style.cursor === 'none'; + }); + /* clang-format on */ +} + +void DisplayServerJavaScript::cursor_set_shape(CursorShape p_shape) { + + ERR_FAIL_INDEX(p_shape, CURSOR_MAX); + + if (mouse_get_mode() == MOUSE_MODE_VISIBLE) { + if (cursors[p_shape] != "") { + Vector<String> url = cursors[p_shape].split("?"); + set_css_cursor(("url(\"" + url[0] + "\") " + url[1] + ", auto").utf8()); + } else { + set_css_cursor(godot2dom_cursor(p_shape)); + } + } + + cursor_shape = p_shape; +} + +DisplayServer::CursorShape DisplayServerJavaScript::cursor_get_shape() const { + return cursor_shape; +} + +void DisplayServerJavaScript::cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { + + if (p_cursor.is_valid()) { + + Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); + + if (cursor_c) { + if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) { + cursor_set_shape(p_shape); + return; + } + + cursors_cache.erase(p_shape); + } + + Ref<Texture2D> texture = p_cursor; + Ref<AtlasTexture> atlas_texture = p_cursor; + Ref<Image> image; + Size2 texture_size; + Rect2 atlas_rect; + + if (texture.is_valid()) { + image = texture->get_data(); + } + + if (!image.is_valid() && atlas_texture.is_valid()) { + texture = atlas_texture->get_atlas(); + + atlas_rect.size.width = texture->get_width(); + atlas_rect.size.height = texture->get_height(); + atlas_rect.position.x = atlas_texture->get_region().position.x; + atlas_rect.position.y = atlas_texture->get_region().position.y; + + texture_size.width = atlas_texture->get_region().size.x; + texture_size.height = atlas_texture->get_region().size.y; + } else if (image.is_valid()) { + texture_size.width = texture->get_width(); + texture_size.height = texture->get_height(); + } + + ERR_FAIL_COND(!texture.is_valid()); + ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); + ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); + ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); + + image = texture->get_data(); + + ERR_FAIL_COND(!image.is_valid()); + + image = image->duplicate(); + + if (atlas_texture.is_valid()) + image->crop_from_point( + atlas_rect.position.x, + atlas_rect.position.y, + texture_size.width, + texture_size.height); + + if (image->get_format() != Image::FORMAT_RGBA8) { + image->convert(Image::FORMAT_RGBA8); + } + + png_image png_meta; + memset(&png_meta, 0, sizeof png_meta); + png_meta.version = PNG_IMAGE_VERSION; + png_meta.width = texture_size.width; + png_meta.height = texture_size.height; + png_meta.format = PNG_FORMAT_RGBA; + + PackedByteArray png; + size_t len; + PackedByteArray data = image->get_data(); + ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); + + png.resize(len); + ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); + + char *object_url; + /* clang-format off */ + EM_ASM({ + var PNG_PTR = $0; + var PNG_LEN = $1; + var PTR = $2; + + var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: 'image/png' }); + var url = URL.createObjectURL(png); + var length_bytes = lengthBytesUTF8(url) + 1; + var string_on_wasm_heap = _malloc(length_bytes); + setValue(PTR, string_on_wasm_heap, '*'); + stringToUTF8(url, string_on_wasm_heap, length_bytes); + }, png.ptr(), len, &object_url); + /* clang-format on */ + + String url = String::utf8(object_url) + "?" + itos(p_hotspot.x) + " " + itos(p_hotspot.y); + + /* clang-format off */ + EM_ASM({ _free($0); }, object_url); + /* clang-format on */ + + if (cursors[p_shape] != "") { + /* clang-format off */ + EM_ASM({ + URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); + }, cursors[p_shape].utf8().get_data()); + /* clang-format on */ + cursors[p_shape] = ""; + } + + cursors[p_shape] = url; + + Vector<Variant> params; + params.push_back(p_cursor); + params.push_back(p_hotspot); + cursors_cache.insert(p_shape, params); + + } else if (cursors[p_shape] != "") { + /* clang-format off */ + EM_ASM({ + URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); + }, cursors[p_shape].utf8().get_data()); + /* clang-format on */ + cursors[p_shape] = ""; + + cursors_cache.erase(p_shape); + } + + cursor_set_shape(cursor_shape); +} + +// Mouse mode +void DisplayServerJavaScript::mouse_set_mode(MouseMode p_mode) { + + ERR_FAIL_COND_MSG(p_mode == MOUSE_MODE_CONFINED, "MOUSE_MODE_CONFINED is not supported for the HTML5 platform."); + if (p_mode == mouse_get_mode()) + return; + + if (p_mode == MOUSE_MODE_VISIBLE) { + + // set_css_cursor must be called before set_cursor_shape to make the cursor visible + set_css_cursor(godot2dom_cursor(cursor_shape)); + cursor_set_shape(cursor_shape); + emscripten_exit_pointerlock(); + + } else if (p_mode == MOUSE_MODE_HIDDEN) { + + set_css_cursor("none"); + emscripten_exit_pointerlock(); + + } else if (p_mode == MOUSE_MODE_CAPTURED) { + + EMSCRIPTEN_RESULT result = emscripten_request_pointerlock("canvas", false); + ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); + ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); + // set_css_cursor must be called before cursor_set_shape to make the cursor visible + set_css_cursor(godot2dom_cursor(cursor_shape)); + cursor_set_shape(cursor_shape); + } +} + +DisplayServer::MouseMode DisplayServerJavaScript::mouse_get_mode() const { + + if (is_css_cursor_hidden()) + return MOUSE_MODE_HIDDEN; + + EmscriptenPointerlockChangeEvent ev; + emscripten_get_pointerlock_status(&ev); + return (ev.isActive && String::utf8(ev.id) == "canvas") ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; +} + +// Wheel + +EM_BOOL DisplayServerJavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data) { + + ERR_FAIL_COND_V(p_event_type != EMSCRIPTEN_EVENT_WHEEL, false); + if (!is_canvas_focused()) { + if (cursor_inside_canvas) { + focus_canvas(); + } else { + return false; + } + } + + Input *input = Input::get_singleton(); + Ref<InputEventMouseButton> ev; + ev.instance(); + ev->set_position(input->get_mouse_position()); + ev->set_global_position(ev->get_position()); + + ev->set_shift(input->is_key_pressed(KEY_SHIFT)); + ev->set_alt(input->is_key_pressed(KEY_ALT)); + ev->set_control(input->is_key_pressed(KEY_CONTROL)); + ev->set_metakey(input->is_key_pressed(KEY_META)); + + if (p_event->deltaY < 0) + ev->set_button_index(BUTTON_WHEEL_UP); + else if (p_event->deltaY > 0) + ev->set_button_index(BUTTON_WHEEL_DOWN); + else if (p_event->deltaX > 0) + ev->set_button_index(BUTTON_WHEEL_LEFT); + else if (p_event->deltaX < 0) + ev->set_button_index(BUTTON_WHEEL_RIGHT); + else + return false; + + // Different browsers give wildly different delta values, and we can't + // interpret deltaMode, so use default value for wheel events' factor. + + int button_flag = 1 << (ev->get_button_index() - 1); + + ev->set_pressed(true); + ev->set_button_mask(input->get_mouse_button_mask() | button_flag); + input->parse_input_event(ev); + + ev->set_pressed(false); + ev->set_button_mask(input->get_mouse_button_mask() & ~button_flag); + input->parse_input_event(ev); + + return true; +} + +// Touch +EM_BOOL DisplayServerJavaScript::touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { + + DisplayServerJavaScript *display = get_singleton(); + Ref<InputEventScreenTouch> ev; + ev.instance(); + int lowest_id_index = -1; + for (int i = 0; i < p_event->numTouches; ++i) { + + const EmscriptenTouchPoint &touch = p_event->touches[i]; + if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) + lowest_id_index = i; + if (!touch.isChanged) + continue; + ev->set_index(touch.identifier); + ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); + display->touches[i] = ev->get_position(); + ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_TOUCHSTART); + + Input::get_singleton()->parse_input_event(ev); + } + // Resume audio context after input in case autoplay was denied. + return true; +} + +EM_BOOL DisplayServerJavaScript::touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { + + DisplayServerJavaScript *display = get_singleton(); + Ref<InputEventScreenDrag> ev; + ev.instance(); + int lowest_id_index = -1; + for (int i = 0; i < p_event->numTouches; ++i) { + + const EmscriptenTouchPoint &touch = p_event->touches[i]; + if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) + lowest_id_index = i; + if (!touch.isChanged) + continue; + ev->set_index(touch.identifier); + ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); + Point2 &prev = display->touches[i]; + ev->set_relative(ev->get_position() - prev); + prev = ev->get_position(); + + Input::get_singleton()->parse_input_event(ev); + } + return true; +} + +bool DisplayServerJavaScript::screen_is_touchscreen(int p_screen) const { + return EM_ASM_INT({ return 'ontouchstart' in window; }); +} + +// Gamepad + +EM_BOOL DisplayServerJavaScript::gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data) { + + Input *input = Input::get_singleton(); + if (p_event_type == EMSCRIPTEN_EVENT_GAMEPADCONNECTED) { + + String guid = ""; + if (String::utf8(p_event->mapping) == "standard") + guid = "Default HTML5 Gamepad"; + input->joy_connection_changed(p_event->index, true, String::utf8(p_event->id), guid); + } else { + input->joy_connection_changed(p_event->index, false, ""); + } + return true; +} + +void DisplayServerJavaScript::process_joypads() { + + int joypad_count = emscripten_get_num_gamepads(); + Input *input = Input::get_singleton(); + for (int joypad = 0; joypad < joypad_count; joypad++) { + EmscriptenGamepadEvent state; + EMSCRIPTEN_RESULT query_result = emscripten_get_gamepad_status(joypad, &state); + // Chromium reserves gamepads slots, so NO_DATA is an expected result. + ERR_CONTINUE(query_result != EMSCRIPTEN_RESULT_SUCCESS && + query_result != EMSCRIPTEN_RESULT_NO_DATA); + if (query_result == EMSCRIPTEN_RESULT_SUCCESS && state.connected) { + + int button_count = MIN(state.numButtons, 18); + int axis_count = MIN(state.numAxes, 8); + for (int button = 0; button < button_count; button++) { + + float value = state.analogButton[button]; + if (String::utf8(state.mapping) == "standard" && (button == JOY_ANALOG_L2 || button == JOY_ANALOG_R2)) { + Input::JoyAxis joy_axis; + joy_axis.min = 0; + joy_axis.value = value; + input->joy_axis(joypad, button, joy_axis); + } else { + input->joy_button(joypad, button, value); + } + } + for (int axis = 0; axis < axis_count; axis++) { + + Input::JoyAxis joy_axis; + joy_axis.min = -1; + joy_axis.value = state.axis[axis]; + input->joy_axis(joypad, axis, joy_axis); + } + } + } +} + +#if 0 +bool DisplayServerJavaScript::is_joy_known(int p_device) { + + return Input::get_singleton()->is_joy_mapped(p_device); +} + +String DisplayServerJavaScript::get_joy_guid(int p_device) const { + + return Input::get_singleton()->get_joy_guid_remapped(p_device); +} +#endif + +Vector<String> DisplayServerJavaScript::get_rendering_drivers_func() { + Vector<String> drivers; + drivers.push_back("dummy"); + return drivers; +} + +// Clipboard +extern "C" EMSCRIPTEN_KEEPALIVE void update_clipboard(const char *p_text) { + // Only call set_clipboard from OS (sets local clipboard) + DisplayServerJavaScript::get_singleton()->clipboard = p_text; +} + +void DisplayServerJavaScript::clipboard_set(const String &p_text) { + /* clang-format off */ + int err = EM_ASM_INT({ + var text = UTF8ToString($0); + if (!navigator.clipboard || !navigator.clipboard.writeText) + return 1; + navigator.clipboard.writeText(text).catch(function(e) { + // Setting OS clipboard is only possible from an input callback. + console.error("Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:", e); + }); + return 0; + }, p_text.utf8().get_data()); + /* clang-format on */ + ERR_FAIL_COND_MSG(err, "Clipboard API is not supported."); +} + +String DisplayServerJavaScript::clipboard_get() const { + /* clang-format off */ + EM_ASM({ + try { + navigator.clipboard.readText().then(function (result) { + ccall('update_clipboard', 'void', ['string'], [result]); + }).catch(function (e) { + // Fail graciously. + }); + } catch (e) { + // Fail graciously. + } + }); + /* clang-format on */ + return clipboard; +} + +extern "C" EMSCRIPTEN_KEEPALIVE void send_window_event(int p_notification) { + + if (p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER || p_notification == DisplayServer::WINDOW_EVENT_MOUSE_EXIT) { + cursor_inside_canvas = p_notification == DisplayServer::WINDOW_EVENT_MOUSE_ENTER; + } + OS_JavaScript *os = OS_JavaScript::get_singleton(); + if (os->is_finalizing()) + return; // We don't want events anymore. + DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); + if (ds && !ds->window_event_callback.is_null()) { + Variant event = int(p_notification); + Variant *eventp = &event; + Variant ret; + Callable::CallError ce; + ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce); + } +} + +void DisplayServerJavaScript::alert(const String &p_alert, const String &p_title) { + + /* clang-format off */ + EM_ASM_({ + window.alert(UTF8ToString($0)); + }, p_alert.utf8().get_data()); + /* clang-format on */ +} + +void DisplayServerJavaScript::set_icon(const Ref<Image> &p_icon) { + + ERR_FAIL_COND(p_icon.is_null()); + Ref<Image> icon = p_icon; + if (icon->is_compressed()) { + icon = icon->duplicate(); + ERR_FAIL_COND(icon->decompress() != OK); + } + if (icon->get_format() != Image::FORMAT_RGBA8) { + if (icon == p_icon) + icon = icon->duplicate(); + icon->convert(Image::FORMAT_RGBA8); + } + + png_image png_meta; + memset(&png_meta, 0, sizeof png_meta); + png_meta.version = PNG_IMAGE_VERSION; + png_meta.width = icon->get_width(); + png_meta.height = icon->get_height(); + png_meta.format = PNG_FORMAT_RGBA; + + PackedByteArray png; + size_t len; + PackedByteArray data = icon->get_data(); + ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); + + png.resize(len); + ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); + + /* clang-format off */ + EM_ASM({ + var PNG_PTR = $0; + var PNG_LEN = $1; + + var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: "image/png" }); + var url = URL.createObjectURL(png); + var link = document.getElementById('-gd-engine-icon'); + if (link === null) { + link = document.createElement('link'); + link.rel = 'icon'; + link.id = '-gd-engine-icon'; + document.head.appendChild(link); + } + link.href = url; + }, png.ptr(), len); + /* clang-format on */ +} + +void DisplayServerJavaScript::_dispatch_input_event(const Ref<InputEvent> &p_event) { + OS_JavaScript *os = OS_JavaScript::get_singleton(); + if (os->is_finalizing()) + return; // We don't want events anymore. + + // Resume audio context after input in case autoplay was denied. + os->resume_audio(); + + Callable cb = get_singleton()->input_event_callback; + if (!cb.is_null()) { + Variant ev = p_event; + Variant *evp = &ev; + Variant ret; + Callable::CallError ce; + cb.call((const Variant **)&evp, 1, ret, ce); + } +} + +DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + return memnew(DisplayServerJavaScript(p_rendering_driver, p_mode, p_flags, p_resolution, r_error)); +} + +DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + + /* clang-format off */ + EM_ASM({ + const canvas = Module['canvas']; + var enc = new TextEncoder("utf-8"); + var buffer = new Uint8Array(enc.encode(canvas.id)); + var len = buffer.byteLength; + var out = _malloc(len); + HEAPU8.set(buffer, out); + ccall("_set_canvas_id", + "void", + ["number", "number"], + [out, len] + ); + _free(out); + }); + /* clang-format on */ + + RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? +#if 0 + EmscriptenWebGLContextAttributes attributes; + emscripten_webgl_init_context_attributes(&attributes); + attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed"); + attributes.antialias = false; + ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER); + + if (p_desired.layered) { + set_window_per_pixel_transparency_enabled(true); + } + + bool gl_initialization_error = false; + + if (RasterizerGLES2::is_viable() == OK) { + attributes.majorVersion = 1; + RasterizerGLES2::register_config(); + RasterizerGLES2::make_current(); + } else { + gl_initialization_error = true; + } + + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(canvas_id.utf8().get_data(), &attributes); + if (emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS) { + gl_initialization_error = true; + } + + if (gl_initialization_error) { + OS::get_singleton()->alert("Your browser does not seem to support WebGL. Please update your browser version.", + "Unable to initialize video driver"); + return ERR_UNAVAILABLE; + } + + video_driver_index = p_video_driver; +#endif + + /* clang-format off */ + window_set_mode(p_mode); + if (EM_ASM_INT_V({ return Module['resizeCanvasOnStart'] })) { + /* clang-format on */ + window_set_size(p_resolution); + } + + EMSCRIPTEN_RESULT result; + CharString id = canvas_id.utf8(); +#define EM_CHECK(ev) \ + if (result != EMSCRIPTEN_RESULT_SUCCESS) \ + ERR_PRINT("Error while setting " #ev " callback: Code " + itos(result)); +#define SET_EM_CALLBACK(target, ev, cb) \ + result = emscripten_set_##ev##_callback(target, nullptr, true, &cb); \ + EM_CHECK(ev) +#define SET_EM_CALLBACK_NOTARGET(ev, cb) \ + result = emscripten_set_##ev##_callback(nullptr, true, &cb); \ + EM_CHECK(ev) + // These callbacks from Emscripten's html5.h suffice to access most + // JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM + // is used below. + SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback) + SET_EM_CALLBACK(id.get_data(), mousedown, mouse_button_callback) + SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback) + SET_EM_CALLBACK(id.get_data(), wheel, wheel_callback) + SET_EM_CALLBACK(id.get_data(), touchstart, touch_press_callback) + SET_EM_CALLBACK(id.get_data(), touchmove, touchmove_callback) + SET_EM_CALLBACK(id.get_data(), touchend, touch_press_callback) + SET_EM_CALLBACK(id.get_data(), touchcancel, touch_press_callback) + SET_EM_CALLBACK(id.get_data(), keydown, keydown_callback) + SET_EM_CALLBACK(id.get_data(), keypress, keypress_callback) + SET_EM_CALLBACK(id.get_data(), keyup, keyup_callback) + SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback) + SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback) + SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback) +#undef SET_EM_CALLBACK_NOTARGET +#undef SET_EM_CALLBACK +#undef EM_CHECK + + /* clang-format off */ + EM_ASM_ARGS({ + Module.listeners = {}; + const canvas = Module['canvas']; + const send_window_event = cwrap('send_window_event', null, ['number']); + const notifications = arguments; + (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { + Module.listeners[event] = send_window_event.bind(null, notifications[index]); + canvas.addEventListener(event, Module.listeners[event]); + }); + // Clipboard + const update_clipboard = cwrap('update_clipboard', null, ['string']); + Module.listeners['paste'] = function(evt) { + update_clipboard(evt.clipboardData.getData('text')); + }; + window.addEventListener('paste', Module.listeners['paste'], false); + Module.listeners['dragover'] = function(ev) { + // Prevent default behavior (which would try to open the file(s)) + ev.preventDefault(); + }; + Module.listeners['drop'] = Module.drop_handler; // Defined in native/utils.js + canvas.addEventListener('dragover', Module.listeners['dragover'], false); + canvas.addEventListener('drop', Module.listeners['drop'], false); + }, + WINDOW_EVENT_MOUSE_ENTER, + WINDOW_EVENT_MOUSE_EXIT, + WINDOW_EVENT_FOCUS_IN, + WINDOW_EVENT_FOCUS_OUT + ); + /* clang-format on */ + + Input::get_singleton()->set_event_dispatch_function(_dispatch_input_event); +} + +DisplayServerJavaScript::~DisplayServerJavaScript() { + EM_ASM({ + Object.entries(Module.listeners).forEach(function(kv) { + if (kv[0] == 'paste') { + window.removeEventListener(kv[0], kv[1], true); + } else { + Module['canvas'].removeEventListener(kv[0], kv[1]); + } + }); + Module.listeners = {}; + }); + //emscripten_webgl_commit_frame(); + //emscripten_webgl_destroy_context(webgl_ctx); +} + +bool DisplayServerJavaScript::has_feature(Feature p_feature) const { + switch (p_feature) { + //case FEATURE_CONSOLE_WINDOW: + //case FEATURE_GLOBAL_MENU: + //case FEATURE_HIDPI: + //case FEATURE_IME: + case FEATURE_ICON: + case FEATURE_CLIPBOARD: + case FEATURE_CURSOR_SHAPE: + case FEATURE_CUSTOM_CURSOR_SHAPE: + case FEATURE_MOUSE: + case FEATURE_TOUCHSCREEN: + return true; + //case FEATURE_MOUSE_WARP: + //case FEATURE_NATIVE_DIALOG: + //case FEATURE_NATIVE_ICON: + //case FEATURE_NATIVE_VIDEO: + //case FEATURE_WINDOW_TRANSPARENCY: + //case FEATURE_KEEP_SCREEN_ON: + //case FEATURE_ORIENTATION: + //case FEATURE_VIRTUAL_KEYBOARD: + default: + return false; + } +} + +void DisplayServerJavaScript::register_javascript_driver() { + register_create_function("javascript", create_func, get_rendering_drivers_func); +} + +String DisplayServerJavaScript::get_name() const { + return "javascript"; +} + +int DisplayServerJavaScript::get_screen_count() const { + return 1; +} + +Point2i DisplayServerJavaScript::screen_get_position(int p_screen) const { + return Point2i(); // TODO offsetX/Y? +} + +Size2i DisplayServerJavaScript::screen_get_size(int p_screen) const { + + EmscriptenFullscreenChangeEvent ev; + EMSCRIPTEN_RESULT result = emscripten_get_fullscreen_status(&ev); + ERR_FAIL_COND_V(result != EMSCRIPTEN_RESULT_SUCCESS, Size2i()); + return Size2i(ev.screenWidth, ev.screenHeight); +} + +Rect2i DisplayServerJavaScript::screen_get_usable_rect(int p_screen) const { + int canvas[2]; + emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1); + return Rect2i(0, 0, canvas[0], canvas[1]); +} + +int DisplayServerJavaScript::screen_get_dpi(int p_screen) const { + return 96; // TODO maybe check pixel ratio via window.devicePixelRatio * 96? Inexact. +} + +Vector<DisplayServer::WindowID> DisplayServerJavaScript::get_window_list() const { + Vector<WindowID> ret; + ret.push_back(MAIN_WINDOW_ID); + return ret; +} + +DisplayServerJavaScript::WindowID DisplayServerJavaScript::get_window_at_screen_position(const Point2i &p_position) const { + return MAIN_WINDOW_ID; +} + +void DisplayServerJavaScript::window_attach_instance_id(ObjectID p_instance, WindowID p_window) { + window_attached_instance_id = p_instance; +} + +ObjectID DisplayServerJavaScript::window_get_attached_instance_id(WindowID p_window) const { + return window_attached_instance_id; +} + +void DisplayServerJavaScript::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) { + // Not supported. +} + +void DisplayServerJavaScript::window_set_window_event_callback(const Callable &p_callable, WindowID p_window) { + window_event_callback = p_callable; +} + +void DisplayServerJavaScript::window_set_input_event_callback(const Callable &p_callable, WindowID p_window) { + input_event_callback = p_callable; +} + +void DisplayServerJavaScript::window_set_input_text_callback(const Callable &p_callable, WindowID p_window) { + input_text_callback = p_callable; // TODO unused... do I need this? +} + +void DisplayServerJavaScript::window_set_drop_files_callback(const Callable &p_callable, WindowID p_window) { + drop_files_callback = p_callable; +} + +void DisplayServerJavaScript::window_set_title(const String &p_title, WindowID p_window) { + /* clang-format off */ + EM_ASM_({ + document.title = UTF8ToString($0); + }, p_title.utf8().get_data()); + /* clang-format on */ +} + +int DisplayServerJavaScript::window_get_current_screen(WindowID p_window) const { + return 1; +} + +void DisplayServerJavaScript::window_set_current_screen(int p_screen, WindowID p_window) { + // Not implemented. +} + +Point2i DisplayServerJavaScript::window_get_position(WindowID p_window) const { + return Point2i(); // TODO Does this need implementation? +} +void DisplayServerJavaScript::window_set_position(const Point2i &p_position, WindowID p_window) { + // Not supported. +} + +void DisplayServerJavaScript::window_set_transient(WindowID p_window, WindowID p_parent) { + // Not supported. +} + +void DisplayServerJavaScript::window_set_max_size(const Size2i p_size, WindowID p_window) { + // Not supported. +} + +Size2i DisplayServerJavaScript::window_get_max_size(WindowID p_window) const { + return Size2i(); +} + +void DisplayServerJavaScript::window_set_min_size(const Size2i p_size, WindowID p_window) { + // Not supported. +} + +Size2i DisplayServerJavaScript::window_get_min_size(WindowID p_window) const { + return Size2i(); +} + +void DisplayServerJavaScript::window_set_size(const Size2i p_size, WindowID p_window) { + emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), p_size.x, p_size.y); +} + +Size2i DisplayServerJavaScript::window_get_size(WindowID p_window) const { + int canvas[2]; + emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1); + return Size2(canvas[0], canvas[1]); +} + +Size2i DisplayServerJavaScript::window_get_real_size(WindowID p_window) const { + return window_get_size(p_window); +} + +void DisplayServerJavaScript::window_set_mode(WindowMode p_mode, WindowID p_window) { + if (window_mode == p_mode) + return; + + switch (p_mode) { + case WINDOW_MODE_WINDOWED: { + if (window_mode == WINDOW_MODE_FULLSCREEN) { + emscripten_exit_fullscreen(); + } + window_mode = WINDOW_MODE_WINDOWED; + window_set_size(windowed_size); + } break; + case WINDOW_MODE_FULLSCREEN: { + EmscriptenFullscreenStrategy strategy; + strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; + strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; + strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; + strategy.canvasResizedCallback = nullptr; + EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(canvas_id.utf8().get_data(), false, &strategy); + ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "Enabling fullscreen is only possible from an input callback for the HTML5 platform."); + ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "Enabling fullscreen is only possible from an input callback for the HTML5 platform."); + } break; + case WINDOW_MODE_MAXIMIZED: + case WINDOW_MODE_MINIMIZED: + WARN_PRINT("WindowMode MAXIMIZED and MINIMIZED are not supported in HTML5 platform."); + break; + default: + break; + } +} + +DisplayServerJavaScript::WindowMode DisplayServerJavaScript::window_get_mode(WindowID p_window) const { + return window_mode; +} + +bool DisplayServerJavaScript::window_is_maximize_allowed(WindowID p_window) const { + return false; +} + +void DisplayServerJavaScript::window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window) { + // Not supported. +} + +bool DisplayServerJavaScript::window_get_flag(WindowFlags p_flag, WindowID p_window) const { + return false; +} + +void DisplayServerJavaScript::window_request_attention(WindowID p_window) { + // Not supported. +} + +void DisplayServerJavaScript::window_move_to_foreground(WindowID p_window) { + // Not supported. +} + +bool DisplayServerJavaScript::window_can_draw(WindowID p_window) const { + return true; +} + +bool DisplayServerJavaScript::can_any_window_draw() const { + return true; +} + +void DisplayServerJavaScript::process_events() { + if (emscripten_sample_gamepad_data() == EMSCRIPTEN_RESULT_SUCCESS) + process_joypads(); +} + +int DisplayServerJavaScript::get_current_video_driver() const { + return 1; +} + +void DisplayServerJavaScript::swap_buffers() { + //emscripten_webgl_commit_frame(); +} diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h new file mode 100644 index 0000000000..73a7b017e6 --- /dev/null +++ b/platform/javascript/display_server_javascript.h @@ -0,0 +1,188 @@ +/*************************************************************************/ +/* display_server_javascript.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef DISPLAY_SERVER_JAVASCRIPT_H +#define DISPLAY_SERVER_JAVASCRIPT_H + +#include "servers/display_server.h" + +#include <emscripten.h> +#include <emscripten/html5.h> + +class DisplayServerJavaScript : public DisplayServer { + + //int video_driver_index; + + Vector2 windowed_size; + + ObjectID window_attached_instance_id = {}; + + Ref<InputEventKey> deferred_key_event; + CursorShape cursor_shape = CURSOR_ARROW; + String cursors[CURSOR_MAX]; + Map<CursorShape, Vector<Variant>> cursors_cache; + Point2 touches[32]; + + Point2i last_click_pos = Point2(-100, -100); // TODO check this again. + double last_click_ms = 0; + int last_click_button_index = -1; + + static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data); + + static EM_BOOL keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); + static EM_BOOL keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); + static EM_BOOL keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); + + static EM_BOOL mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); + static EM_BOOL mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); + + static EM_BOOL wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data); + + static EM_BOOL touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); + static EM_BOOL touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); + + static EM_BOOL gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data); + void process_joypads(); + + static Vector<String> get_rendering_drivers_func(); + static DisplayServer *create_func(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + + static void _dispatch_input_event(const Ref<InputEvent> &p_event); + +protected: + virtual int get_current_video_driver() const; + +public: + // Override return type to make writing static callbacks less tedious. + static DisplayServerJavaScript *get_singleton(); + + WindowMode window_mode = WINDOW_MODE_WINDOWED; + + String clipboard; + String canvas_id; + + Callable window_event_callback; + Callable input_event_callback; + Callable input_text_callback; + Callable drop_files_callback; + + // from DisplayServer + virtual void alert(const String &p_alert, const String &p_title = "ALERT!"); + virtual bool has_feature(Feature p_feature) const; + virtual String get_name() const; + + // cursor + virtual void cursor_set_shape(CursorShape p_shape); + virtual CursorShape cursor_get_shape() const; + virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()); + + // mouse + virtual void mouse_set_mode(MouseMode p_mode); + virtual MouseMode mouse_get_mode() const; + + // touch + virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + + // clipboard + virtual void clipboard_set(const String &p_text); + virtual String clipboard_get() const; + + // screen + virtual int get_screen_count() const; + virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + + // windows + virtual Vector<DisplayServer::WindowID> get_window_list() const; + virtual WindowID get_window_at_screen_position(const Point2i &p_position) const; + + virtual void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID); + virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + + virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + virtual void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + + virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + + virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID); + + virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const; + virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID); + + virtual Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const; + virtual void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID); + + virtual void window_set_transient(WindowID p_window, WindowID p_parent); + + virtual void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID); + virtual Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID); + virtual Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID); + virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const; + virtual Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const; // FIXME: Find clearer name for this. + + virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID); + virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID); + virtual bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const; + + virtual void window_request_attention(WindowID p_window = MAIN_WINDOW_ID); + virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID); + + virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual bool can_any_window_draw() const; + + // events + virtual void process_events(); + + // icon + virtual void set_icon(const Ref<Image> &p_icon); + + // others + virtual void swap_buffers(); + + static void register_javascript_driver(); + DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + ~DisplayServerJavaScript(); +}; + +#endif // DISPLAY_SERVER_JAVASCRIPT_H diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js index 6d7509377f..d709422abb 100644 --- a/platform/javascript/engine/engine.js +++ b/platform/javascript/engine/engine.js @@ -1,16 +1,8 @@ Function('return this')()['Engine'] = (function() { - - var unloadAfterInit = true; - var canvas = null; - var resizeCanvasOnStart = false; - var customLocale = 'en_US'; - var wasmExt = '.wasm'; - var preloader = new Preloader(); - var loader = new Loader(); - var rtenv = null; - var executableName = ''; + var wasmExt = '.wasm'; + var unloadAfterInit = true; var loadPath = ''; var loadPromise = null; var initPromise = null; @@ -33,31 +25,43 @@ Function('return this')()['Engine'] = (function() { }; /** @constructor */ - function Engine() {}; + function Engine() { + this.canvas = null; + this.executableName = ''; + this.rtenv = null; + this.customLocale = null; + this.resizeCanvasOnStart = false; + this.onExecute = null; + this.onExit = null; + }; Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { if (initPromise) { return initPromise; } - if (!loadPromise) { + if (loadPromise == null) { if (!basePath) { initPromise = Promise.reject(new Error("A base path must be provided when calling `init` and the engine is not loaded.")); return initPromise; } load(basePath); } - var config = {} + var config = {}; if (typeof stdout === 'function') config.print = stdout; if (typeof stderr === 'function') config.printErr = stderr; - initPromise = loader.init(loadPromise, loadPath, config).then(function() { - return new Promise(function(resolve, reject) { - rtenv = loader.env; + var me = this; + initPromise = new Promise(function(resolve, reject) { + config['locateFile'] = Utils.createLocateRewrite(loadPath); + config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); + Godot(config).then(function(module) { + me.rtenv = module; if (unloadAfterInit) { - loadPromise = null; + unload(); } resolve(); + config = null; }); }); return initPromise; @@ -76,33 +80,72 @@ Function('return this')()['Engine'] = (function() { args.push(arguments[i]); } var me = this; - return new Promise(function(resolve, reject) { - return me.init().then(function() { - if (!(canvas instanceof HTMLCanvasElement)) { - canvas = Utils.findCanvas(); - } - rtenv['locale'] = customLocale; - rtenv['canvas'] = canvas; - rtenv['thisProgram'] = executableName; - rtenv['resizeCanvasOnStart'] = resizeCanvasOnStart; - loader.start(preloader.preloadedFiles, args).then(function() { - loader = null; - initPromise = null; - resolve(); + return me.init().then(function() { + if (!me.rtenv) { + return Promise.reject(new Error('The engine must be initialized before it can be started')); + } + + if (!(me.canvas instanceof HTMLCanvasElement)) { + me.canvas = Utils.findCanvas(); + } + + // Canvas can grab focus on click, or key events won't work. + if (me.canvas.tabIndex < 0) { + me.canvas.tabIndex = 0; + } + + // Disable right-click context menu. + me.canvas.addEventListener('contextmenu', function(ev) { + ev.preventDefault(); + }, false); + + // Until context restoration is implemented warn the user of context loss. + me.canvas.addEventListener('webglcontextlost', function(ev) { + alert("WebGL context lost, please reload the page"); + ev.preventDefault(); + }, false); + + // Browser locale, or custom one if defined. + var locale = me.customLocale; + if (!locale) { + locale = navigator.languages ? navigator.languages[0] : navigator.language; + locale = locale.split('.')[0]; + } + me.rtenv['locale'] = locale; + me.rtenv['canvas'] = me.canvas; + me.rtenv['thisProgram'] = me.executableName; + me.rtenv['resizeCanvasOnStart'] = me.resizeCanvasOnStart; + me.rtenv['noExitRuntime'] = true; + me.rtenv['onExecute'] = me.onExecute; + me.rtenv['onExit'] = function(code) { + if (me.onExit) + me.onExit(code); + me.rtenv = null; + } + return new Promise(function(resolve, reject) { + preloader.preloadedFiles.forEach(function(file) { + me.rtenv['copyToFS'](file.path, file.buffer); }); + preloader.preloadedFiles.length = 0; // Clear memory + me.rtenv['callMain'](args); + initPromise = null; + resolve(); }); }); }; - Engine.prototype.startGame = function(execName, mainPack) { + Engine.prototype.startGame = function(execName, mainPack, extraArgs) { // Start and init with execName as loadPath if not inited. - executableName = execName; + this.executableName = execName; var me = this; return Promise.all([ this.init(execName), this.preloadFile(mainPack, mainPack) ]).then(function() { - return me.start('--main-pack', mainPack); + var args = ['--main-pack', mainPack]; + if (extraArgs) + args = args.concat(extraArgs); + return me.start.apply(me, args); }); }; @@ -118,67 +161,85 @@ Function('return this')()['Engine'] = (function() { }; Engine.prototype.setCanvas = function(canvasElem) { - canvas = canvasElem; + this.canvas = canvasElem; }; Engine.prototype.setCanvasResizedOnStart = function(enabled) { - resizeCanvasOnStart = enabled; + this.resizeCanvasOnStart = enabled; }; Engine.prototype.setLocale = function(locale) { - customLocale = locale; + this.customLocale = locale; }; Engine.prototype.setExecutableName = function(newName) { - executableName = newName; + this.executableName = newName; }; Engine.prototype.setProgressFunc = function(func) { progressFunc = func; - } + }; Engine.prototype.setStdoutFunc = function(func) { - var print = function(text) { if (arguments.length > 1) { text = Array.prototype.slice.call(arguments).join(" "); } func(text); }; - if (rtenv) - rtenv.print = print; + if (this.rtenv) + this.rtenv.print = print; stdout = print; }; Engine.prototype.setStderrFunc = function(func) { - var printErr = function(text) { if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(" "); func(text); }; - if (rtenv) - rtenv.printErr = printErr; + if (this.rtenv) + this.rtenv.printErr = printErr; stderr = printErr; }; + Engine.prototype.setOnExecute = function(onExecute) { + if (this.rtenv) + this.rtenv.onExecute = onExecute; + this.onExecute = onExecute; + } + + Engine.prototype.setOnExit = function(onExit) { + this.onExit = onExit; + } + + Engine.prototype.copyToFS = function(path, buffer) { + if (this.rtenv == null) { + throw new Error("Engine must be inited before copying files"); + } + this.rtenv['copyToFS'](path, buffer); + } + // Closure compiler exported engine methods. /** @export */ Engine['isWebGLAvailable'] = Utils.isWebGLAvailable; Engine['load'] = load; Engine['unload'] = unload; - Engine.prototype['init'] = Engine.prototype.init - Engine.prototype['preloadFile'] = Engine.prototype.preloadFile - Engine.prototype['start'] = Engine.prototype.start - Engine.prototype['startGame'] = Engine.prototype.startGame - Engine.prototype['setWebAssemblyFilenameExtension'] = Engine.prototype.setWebAssemblyFilenameExtension - Engine.prototype['setUnloadAfterInit'] = Engine.prototype.setUnloadAfterInit - Engine.prototype['setCanvas'] = Engine.prototype.setCanvas - Engine.prototype['setCanvasResizedOnStart'] = Engine.prototype.setCanvasResizedOnStart - Engine.prototype['setLocale'] = Engine.prototype.setLocale - Engine.prototype['setExecutableName'] = Engine.prototype.setExecutableName - Engine.prototype['setProgressFunc'] = Engine.prototype.setProgressFunc - Engine.prototype['setStdoutFunc'] = Engine.prototype.setStdoutFunc - Engine.prototype['setStderrFunc'] = Engine.prototype.setStderrFunc + Engine.prototype['init'] = Engine.prototype.init; + Engine.prototype['preloadFile'] = Engine.prototype.preloadFile; + Engine.prototype['start'] = Engine.prototype.start; + Engine.prototype['startGame'] = Engine.prototype.startGame; + Engine.prototype['setWebAssemblyFilenameExtension'] = Engine.prototype.setWebAssemblyFilenameExtension; + Engine.prototype['setUnloadAfterInit'] = Engine.prototype.setUnloadAfterInit; + Engine.prototype['setCanvas'] = Engine.prototype.setCanvas; + Engine.prototype['setCanvasResizedOnStart'] = Engine.prototype.setCanvasResizedOnStart; + Engine.prototype['setLocale'] = Engine.prototype.setLocale; + Engine.prototype['setExecutableName'] = Engine.prototype.setExecutableName; + Engine.prototype['setProgressFunc'] = Engine.prototype.setProgressFunc; + Engine.prototype['setStdoutFunc'] = Engine.prototype.setStdoutFunc; + Engine.prototype['setStderrFunc'] = Engine.prototype.setStderrFunc; + Engine.prototype['setOnExecute'] = Engine.prototype.setOnExecute; + Engine.prototype['setOnExit'] = Engine.prototype.setOnExit; + Engine.prototype['copyToFS'] = Engine.prototype.copyToFS; return Engine; })(); diff --git a/platform/javascript/engine/loader.js b/platform/javascript/engine/loader.js deleted file mode 100644 index d27fbf612e..0000000000 --- a/platform/javascript/engine/loader.js +++ /dev/null @@ -1,33 +0,0 @@ -var Loader = /** @constructor */ function() { - - this.env = null; - - this.init = function(loadPromise, basePath, config) { - var me = this; - return new Promise(function(resolve, reject) { - var cfg = config || {}; - cfg['locateFile'] = Utils.createLocateRewrite(basePath); - cfg['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); - loadPromise = null; - Godot(cfg).then(function(module) { - me.env = module; - resolve(); - }); - }); - } - - this.start = function(preloadedFiles, args) { - var me = this; - return new Promise(function(resolve, reject) { - if (!me.env) { - reject(new Error('The engine must be initialized before it can be started')); - } - preloadedFiles.forEach(function(file) { - Utils.copyToFS(me.env['FS'], file.path, file.buffer); - }); - preloadedFiles.length = 0; // Clear memory - me.env['callMain'](args); - resolve(); - }); - } -}; diff --git a/platform/javascript/engine/utils.js b/platform/javascript/engine/utils.js index fdff90a923..0c97b38199 100644 --- a/platform/javascript/engine/utils.js +++ b/platform/javascript/engine/utils.js @@ -27,24 +27,6 @@ var Utils = { return instantiateWasm; }, - copyToFS: function(fs, path, buffer) { - var p = path.lastIndexOf("/"); - var dir = "/"; - if (p > 0) { - dir = path.slice(0, path.lastIndexOf("/")); - } - try { - fs.stat(dir); - } catch (e) { - if (e.errno !== 44) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h - throw e; - } - fs['mkdirTree'](dir); - } - // With memory growth, canOwn should be false. - fs['writeFile'](path, new Uint8Array(buffer), {'flags': 'wx+'}); - }, - findCanvas: function() { var nodes = document.getElementsByTagName('canvas'); if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 815bc7e456..854383aeee 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -34,22 +34,63 @@ #include <emscripten/emscripten.h> +static OS_JavaScript *os = nullptr; + +void exit_callback() { + emscripten_cancel_main_loop(); // After this, we can exit! + Main::cleanup(); + int exit_code = OS_JavaScript::get_singleton()->get_exit_code(); + memdelete(os); + os = nullptr; + emscripten_force_exit(exit_code); // No matter that we call cancel_main_loop, regular "exit" will not work, forcing. +} + +void main_loop_callback() { + + if (os->main_loop_iterate()) { + emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async. + EM_ASM({ + // This will contain the list of operations that need to complete before cleanup. + Module.async_finish = []; + }); + os->get_main_loop()->finish(); + os->finalize_async(); // Will add all the async finish functions. + EM_ASM({ + Promise.all(Module.async_finish).then(function() { + Module.async_finish = []; + ccall("cleanup_after_sync", null, []); + }); + }); + } +} + +extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() { + emscripten_set_main_loop(exit_callback, -1, false); +} + extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { String idbfs_err = String::utf8(p_idbfs_err); if (!idbfs_err.empty()) { print_line("IndexedDB not available: " + idbfs_err); } - OS_JavaScript *os = OS_JavaScript::get_singleton(); os->set_idb_available(idbfs_err.empty()); + // TODO: Check error return value. + Main::setup2(); // Manual second phase. // Ease up compatibility. ResourceLoader::set_abort_on_missing_resources(false); Main::start(); - os->run_async(); + os->get_main_loop()->init(); + emscripten_resume_main_loop(); } int main(int argc, char *argv[]) { + os = new OS_JavaScript(); + Main::setup(argv[0], argc - 1, &argv[1], false); + emscripten_set_main_loop(main_loop_callback, -1, false); + emscripten_pause_main_loop(); // Will need to wait for FS sync. + // Sync from persistent state into memory and then // run the 'main_after_fs_sync' function. /* clang-format off */ @@ -59,13 +100,10 @@ int main(int argc, char *argv[]) { FS.syncfs(true, function(err) { ccall('main_after_fs_sync', null, ['string'], [err ? err.message : ""]) }); + ); /* clang-format on */ - new OS_JavaScript(argc, argv); - // TODO: Check error return value. - Main::setup(argv[0], argc - 1, &argv[1]); - return 0; // Continued async in main_after_fs_sync() from the syncfs() callback. } diff --git a/platform/javascript/http_request.js b/platform/javascript/native/http_request.js index f621689f9d..f621689f9d 100644 --- a/platform/javascript/http_request.js +++ b/platform/javascript/native/http_request.js diff --git a/platform/javascript/id_handler.js b/platform/javascript/native/id_handler.js index 67d29075b8..67d29075b8 100644 --- a/platform/javascript/id_handler.js +++ b/platform/javascript/native/id_handler.js diff --git a/platform/javascript/native/utils.js b/platform/javascript/native/utils.js new file mode 100644 index 0000000000..95585d26ae --- /dev/null +++ b/platform/javascript/native/utils.js @@ -0,0 +1,204 @@ +/*************************************************************************/ +/* utils.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +Module['copyToFS'] = function(path, buffer) { + var p = path.lastIndexOf("/"); + var dir = "/"; + if (p > 0) { + dir = path.slice(0, path.lastIndexOf("/")); + } + try { + FS.stat(dir); + } catch (e) { + if (e.errno !== ERRNO_CODES.ENOENT) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h + throw e; + } + FS.mkdirTree(dir); + } + // With memory growth, canOwn should be false. + FS.writeFile(path, new Uint8Array(buffer), {'flags': 'wx+'}); +} + +Module.drop_handler = (function() { + var upload = []; + var uploadPromises = []; + var uploadCallback = null; + + function readFilePromise(entry, path) { + return new Promise(function(resolve, reject) { + entry.file(function(file) { + var reader = new FileReader(); + reader.onload = function() { + var f = { + "path": file.relativePath || file.webkitRelativePath, + "name": file.name, + "type": file.type, + "size": file.size, + "data": reader.result + }; + if (!f['path']) + f['path'] = f['name']; + upload.push(f); + resolve() + }; + reader.onerror = function() { + console.log("Error reading file"); + reject(); + } + + reader.readAsArrayBuffer(file); + + }, function(err) { + console.log("Error!"); + reject(); + }); + }); + } + + function readDirectoryPromise(entry) { + return new Promise(function(resolve, reject) { + var reader = entry.createReader(); + reader.readEntries(function(entries) { + for (var i = 0; i < entries.length; i++) { + var ent = entries[i]; + if (ent.isDirectory) { + uploadPromises.push(readDirectoryPromise(ent)); + } else if (ent.isFile) { + uploadPromises.push(readFilePromise(ent)); + } + } + resolve(); + }); + }); + } + + function processUploadsPromises(resolve, reject) { + if (uploadPromises.length == 0) { + resolve(); + return; + } + uploadPromises.pop().then(function() { + setTimeout(function() { + processUploadsPromises(resolve, reject); + //processUploadsPromises.bind(null, resolve, reject) + }, 0); + }); + } + + function dropFiles(files) { + var args = files || []; + var argc = args.length; + var argv = stackAlloc((argc + 1) * 4); + for (var i = 0; i < argc; i++) { + HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i]); + } + HEAP32[(argv >> 2) + argc] = 0; + // Defined in display_server_javascript.cpp + ccall('_drop_files_callback', 'void', ['number', 'number'], [argv, argc]); + } + + return function(ev) { + ev.preventDefault(); + if (ev.dataTransfer.items) { + // Use DataTransferItemList interface to access the file(s) + for (var i = 0; i < ev.dataTransfer.items.length; i++) { + const item = ev.dataTransfer.items[i]; + var entry = null; + if ("getAsEntry" in item) { + entry = item.getAsEntry(); + } else if ("webkitGetAsEntry" in item) { + entry = item.webkitGetAsEntry(); + } + if (!entry) { + console.error("File upload not supported"); + } else if (entry.isDirectory) { + uploadPromises.push(readDirectoryPromise(entry)); + } else if (entry.isFile) { + uploadPromises.push(readFilePromise(entry)); + } else { + console.error("Unrecognized entry...", entry); + } + } + } else { + console.error("File upload not supported"); + } + uploadCallback = new Promise(processUploadsPromises).then(function() { + const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/"; + var drops = []; + var files = []; + upload.forEach((elem) => { + var path = elem['path']; + Module['copyToFS'](DROP + path, elem['data']); + var idx = path.indexOf("/"); + if (idx == -1) { + // Root file + drops.push(DROP + path); + } else { + // Subdir + var sub = path.substr(0, idx); + idx = sub.indexOf("/"); + if (idx < 0 && drops.indexOf(DROP + sub) == -1) { + drops.push(DROP + sub); + } + } + files.push(DROP + path); + }); + uploadPromises = []; + upload = []; + dropFiles(drops); + var dirs = [DROP.substr(0, DROP.length -1)]; + files.forEach(function (file) { + FS.unlink(file); + var dir = file.replace(DROP, ""); + var idx = dir.lastIndexOf("/"); + while (idx > 0) { + dir = dir.substr(0, idx); + if (dirs.indexOf(DROP + dir) == -1) { + dirs.push(DROP + dir); + } + idx = dir.lastIndexOf("/"); + } + }); + // Remove dirs. + dirs = dirs.sort(function(a, b) { + var al = (a.match(/\//g) || []).length; + var bl = (b.match(/\//g) || []).length; + if (al > bl) + return -1; + else if (al < bl) + return 1; + return 0; + }); + dirs.forEach(function(dir) { + FS.rmdir(dir); + }); + }); + } +})(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index 90075df82e..205644ce51 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -31,692 +31,15 @@ #include "os_javascript.h" #include "core/io/file_access_buffered_fa.h" -//#include "drivers/gles2/rasterizer_gles2.h" -#include "drivers/dummy/rasterizer_dummy.h" +#include "core/io/json.h" #include "drivers/unix/dir_access_unix.h" #include "drivers/unix/file_access_unix.h" #include "main/main.h" -#include "servers/rendering/rendering_server_raster.h" +#include "platform/javascript/display_server_javascript.h" #include <emscripten.h> -#include <png.h> #include <stdlib.h> -#include "dom_keys.inc" - -#define DOM_BUTTON_LEFT 0 -#define DOM_BUTTON_MIDDLE 1 -#define DOM_BUTTON_RIGHT 2 -#define DOM_BUTTON_XBUTTON1 3 -#define DOM_BUTTON_XBUTTON2 4 -#define GODOT_CANVAS_SELECTOR "#canvas" - -// Window (canvas) - -static void focus_canvas() { - - /* clang-format off */ - EM_ASM( - Module.canvas.focus(); - ); - /* clang-format on */ -} - -static bool is_canvas_focused() { - - /* clang-format off */ - return EM_ASM_INT_V( - return document.activeElement == Module.canvas; - ); - /* clang-format on */ -} - -static Point2 compute_position_in_canvas(int x, int y) { - int canvas_x = EM_ASM_INT({ - return document.getElementById('canvas').getBoundingClientRect().x; - }); - int canvas_y = EM_ASM_INT({ - return document.getElementById('canvas').getBoundingClientRect().y; - }); - int canvas_width; - int canvas_height; - emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, &canvas_width, &canvas_height); - - double element_width; - double element_height; - emscripten_get_element_css_size(GODOT_CANVAS_SELECTOR, &element_width, &element_height); - - return Point2((int)(canvas_width / element_width * (x - canvas_x)), - (int)(canvas_height / element_height * (y - canvas_y))); -} - -static bool cursor_inside_canvas = true; - -EM_BOOL OS_JavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - // Empty ID is canvas. - String target_id = String::utf8(p_event->id); - if (target_id.empty() || target_id == "canvas") { - // This event property is the only reliable data on - // browser fullscreen state. - os->video_mode.fullscreen = p_event->isFullscreen; - if (os->video_mode.fullscreen) { - os->entering_fullscreen = false; - } else { - // Restoring maximized window now will cause issues, - // so delay until main_loop_iterate. - os->just_exited_fullscreen = true; - } - } - return false; -} - -void OS_JavaScript::set_video_mode(const VideoMode &p_video_mode, int p_screen) { - - video_mode = p_video_mode; -} - -OS::VideoMode OS_JavaScript::get_video_mode(int p_screen) const { - - return video_mode; -} - -Size2 OS_JavaScript::get_screen_size(int p_screen) const { - - EmscriptenFullscreenChangeEvent ev; - EMSCRIPTEN_RESULT result = emscripten_get_fullscreen_status(&ev); - ERR_FAIL_COND_V(result != EMSCRIPTEN_RESULT_SUCCESS, Size2()); - return Size2(ev.screenWidth, ev.screenHeight); -} - -void OS_JavaScript::set_window_size(const Size2 p_size) { - - windowed_size = p_size; - if (video_mode.fullscreen) { - window_maximized = false; - set_window_fullscreen(false); - } else { - if (window_maximized) { - emscripten_exit_soft_fullscreen(); - window_maximized = false; - } - emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, p_size.x, p_size.y); - } -} - -Size2 OS_JavaScript::get_window_size() const { - - int canvas[2]; - emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, canvas, canvas + 1); - return Size2(canvas[0], canvas[1]); -} - -void OS_JavaScript::set_window_maximized(bool p_enabled) { - - if (video_mode.fullscreen) { - window_maximized = p_enabled; - set_window_fullscreen(false); - } else if (!p_enabled) { - emscripten_exit_soft_fullscreen(); - window_maximized = false; - } else if (!window_maximized) { - // Prevent calling emscripten_enter_soft_fullscreen mutltiple times, - // this would hide page elements permanently. - EmscriptenFullscreenStrategy strategy; - strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; - strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; - strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = nullptr; - emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy); - window_maximized = p_enabled; - } -} - -bool OS_JavaScript::is_window_maximized() const { - - return window_maximized; -} - -void OS_JavaScript::set_window_fullscreen(bool p_enabled) { - - if (p_enabled == video_mode.fullscreen) { - return; - } - - // Just request changes here, if successful, logic continues in - // fullscreen_change_callback. - if (p_enabled) { - if (window_maximized) { - // Soft fullsreen during real fullscreen can cause issues, so exit. - // This must be called before requesting full screen. - emscripten_exit_soft_fullscreen(); - } - EmscriptenFullscreenStrategy strategy; - strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; - strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; - strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = nullptr; - EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(GODOT_CANVAS_SELECTOR, false, &strategy); - ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "Enabling fullscreen is only possible from an input callback for the HTML5 platform."); - ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "Enabling fullscreen is only possible from an input callback for the HTML5 platform."); - // Not fullscreen yet, so prevent "windowed" canvas dimensions from - // being overwritten. - entering_fullscreen = true; - } else { - // No logic allowed here, since exiting w/ ESC key won't use this function. - ERR_FAIL_COND(emscripten_exit_fullscreen() != EMSCRIPTEN_RESULT_SUCCESS); - } -} - -bool OS_JavaScript::is_window_fullscreen() const { - - return video_mode.fullscreen; -} - -void OS_JavaScript::get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen) const { - - Size2 screen = get_screen_size(); - p_list->push_back(OS::VideoMode(screen.width, screen.height, true)); -} - -bool OS_JavaScript::get_window_per_pixel_transparency_enabled() const { - if (!is_layered_allowed()) { - return false; - } - return transparency_enabled; -} - -void OS_JavaScript::set_window_per_pixel_transparency_enabled(bool p_enabled) { - if (!is_layered_allowed()) { - return; - } - transparency_enabled = p_enabled; -} - -// Keys - -template <typename T> -static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event) { - - godot_event->set_shift(emscripten_event_ptr->shiftKey); - godot_event->set_alt(emscripten_event_ptr->altKey); - godot_event->set_control(emscripten_event_ptr->ctrlKey); - godot_event->set_metakey(emscripten_event_ptr->metaKey); -} - -static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) { - - Ref<InputEventKey> ev; - ev.instance(); - ev->set_echo(emscripten_event->repeat); - dom2godot_mod(emscripten_event, ev); - ev->set_keycode(dom2godot_keycode(emscripten_event->keyCode)); - ev->set_physical_keycode(dom2godot_keycode(emscripten_event->keyCode)); - - String unicode = String::utf8(emscripten_event->key); - // Check if empty or multi-character (e.g. `CapsLock`). - if (unicode.length() != 1) { - // Might be empty as well, but better than nonsense. - unicode = String::utf8(emscripten_event->charValue); - } - if (unicode.length() == 1) { - ev->set_unicode(unicode[0]); - } - - return ev; -} - -EM_BOOL OS_JavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - Ref<InputEventKey> ev = setup_key_event(p_event); - ev->set_pressed(true); - if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_keycode())) { - // Defer to keypress event for legacy unicode retrieval. - os->deferred_key_event = ev; - // Do not suppress keypress event. - return false; - } - os->input->parse_input_event(ev); - // Resume audio context after input in case autoplay was denied. - os->audio_driver_javascript.resume(); - return true; -} - -EM_BOOL OS_JavaScript::keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - os->deferred_key_event->set_unicode(p_event->charCode); - os->input->parse_input_event(os->deferred_key_event); - return true; -} - -EM_BOOL OS_JavaScript::keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data) { - - Ref<InputEventKey> ev = setup_key_event(p_event); - ev->set_pressed(false); - get_singleton()->input->parse_input_event(ev); - return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != 0; -} - -// Mouse - -Point2 OS_JavaScript::get_mouse_position() const { - - return input->get_mouse_position(); -} - -int OS_JavaScript::get_mouse_button_state() const { - - return input->get_mouse_button_mask(); -} - -EM_BOOL OS_JavaScript::mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - - Ref<InputEventMouseButton> ev; - ev.instance(); - ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_MOUSEDOWN); - ev->set_position(compute_position_in_canvas(p_event->clientX, p_event->clientY)); - ev->set_global_position(ev->get_position()); - dom2godot_mod(p_event, ev); - - switch (p_event->button) { - case DOM_BUTTON_LEFT: - ev->set_button_index(BUTTON_LEFT); - break; - case DOM_BUTTON_MIDDLE: - ev->set_button_index(BUTTON_MIDDLE); - break; - case DOM_BUTTON_RIGHT: - ev->set_button_index(BUTTON_RIGHT); - break; - case DOM_BUTTON_XBUTTON1: - ev->set_button_index(BUTTON_XBUTTON1); - break; - case DOM_BUTTON_XBUTTON2: - ev->set_button_index(BUTTON_XBUTTON2); - break; - default: - return false; - } - - if (ev->is_pressed()) { - - double diff = emscripten_get_now() - os->last_click_ms; - - if (ev->get_button_index() == os->last_click_button_index) { - - if (diff < 400 && Point2(os->last_click_pos).distance_to(ev->get_position()) < 5) { - - os->last_click_ms = 0; - os->last_click_pos = Point2(-100, -100); - os->last_click_button_index = -1; - ev->set_doubleclick(true); - } - - } else { - os->last_click_button_index = ev->get_button_index(); - } - - if (!ev->is_doubleclick()) { - os->last_click_ms += diff; - os->last_click_pos = ev->get_position(); - } - } - - int mask = os->input->get_mouse_button_mask(); - int button_flag = 1 << (ev->get_button_index() - 1); - if (ev->is_pressed()) { - // Since the event is consumed, focus manually. The containing iframe, - // if exists, may not have focus yet, so focus even if already focused. - focus_canvas(); - mask |= button_flag; - } else if (mask & button_flag) { - mask &= ~button_flag; - } else { - // Received release event, but press was outside the canvas, so ignore. - return false; - } - ev->set_button_mask(mask); - - os->input->parse_input_event(ev); - // Resume audio context after input in case autoplay was denied. - os->audio_driver_javascript.resume(); - // Prevent multi-click text selection and wheel-click scrolling anchor. - // Context menu is prevented through contextmenu event. - return true; -} - -EM_BOOL OS_JavaScript::mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - - int input_mask = os->input->get_mouse_button_mask(); - Point2 pos = compute_position_in_canvas(p_event->clientX, p_event->clientY); - // For motion outside the canvas, only read mouse movement if dragging - // started inside the canvas; imitating desktop app behaviour. - if (!cursor_inside_canvas && !input_mask) - return false; - - Ref<InputEventMouseMotion> ev; - ev.instance(); - dom2godot_mod(p_event, ev); - ev->set_button_mask(input_mask); - - ev->set_position(pos); - ev->set_global_position(ev->get_position()); - - ev->set_relative(Vector2(p_event->movementX, p_event->movementY)); - os->input->set_mouse_position(ev->get_position()); - ev->set_speed(os->input->get_last_mouse_speed()); - - os->input->parse_input_event(ev); - // Don't suppress mouseover/-leave events. - return false; -} - -static const char *godot2dom_cursor(OS::CursorShape p_shape) { - - switch (p_shape) { - case OS::CURSOR_ARROW: - default: - return "auto"; - case OS::CURSOR_IBEAM: - return "text"; - case OS::CURSOR_POINTING_HAND: - return "pointer"; - case OS::CURSOR_CROSS: - return "crosshair"; - case OS::CURSOR_WAIT: - return "progress"; - case OS::CURSOR_BUSY: - return "wait"; - case OS::CURSOR_DRAG: - return "grab"; - case OS::CURSOR_CAN_DROP: - return "grabbing"; - case OS::CURSOR_FORBIDDEN: - return "no-drop"; - case OS::CURSOR_VSIZE: - return "ns-resize"; - case OS::CURSOR_HSIZE: - return "ew-resize"; - case OS::CURSOR_BDIAGSIZE: - return "nesw-resize"; - case OS::CURSOR_FDIAGSIZE: - return "nwse-resize"; - case OS::CURSOR_MOVE: - return "move"; - case OS::CURSOR_VSPLIT: - return "row-resize"; - case OS::CURSOR_HSPLIT: - return "col-resize"; - case OS::CURSOR_HELP: - return "help"; - } -} - -static void set_css_cursor(const char *p_cursor) { - - /* clang-format off */ - EM_ASM_({ - Module.canvas.style.cursor = UTF8ToString($0); - }, p_cursor); - /* clang-format on */ -} - -static bool is_css_cursor_hidden() { - - /* clang-format off */ - return EM_ASM_INT({ - return Module.canvas.style.cursor === 'none'; - }); - /* clang-format on */ -} - -void OS_JavaScript::set_cursor_shape(CursorShape p_shape) { - - ERR_FAIL_INDEX(p_shape, CURSOR_MAX); - - if (get_mouse_mode() == MOUSE_MODE_VISIBLE) { - if (cursors[p_shape] != "") { - Vector<String> url = cursors[p_shape].split("?"); - set_css_cursor(("url(\"" + url[0] + "\") " + url[1] + ", auto").utf8()); - } else { - set_css_cursor(godot2dom_cursor(p_shape)); - } - } - - cursor_shape = p_shape; -} - -void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { - - if (p_cursor.is_valid()) { - - Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); - - if (cursor_c) { - if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) { - set_cursor_shape(p_shape); - return; - } - - cursors_cache.erase(p_shape); - } - - Ref<Texture2D> texture = p_cursor; - Ref<AtlasTexture> atlas_texture = p_cursor; - Ref<Image> image; - Size2 texture_size; - Rect2 atlas_rect; - - if (texture.is_valid()) { - image = texture->get_data(); - if (image.is_valid()) { - image->duplicate(); - } - } - - if (!image.is_valid() && atlas_texture.is_valid()) { - texture = atlas_texture->get_atlas(); - - atlas_rect.size.width = texture->get_width(); - atlas_rect.size.height = texture->get_height(); - atlas_rect.position.x = atlas_texture->get_region().position.x; - atlas_rect.position.y = atlas_texture->get_region().position.y; - - texture_size.width = atlas_texture->get_region().size.x; - texture_size.height = atlas_texture->get_region().size.y; - } else if (image.is_valid()) { - texture_size.width = texture->get_width(); - texture_size.height = texture->get_height(); - } - - ERR_FAIL_COND(!texture.is_valid()); - ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); - ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); - ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - - image = texture->get_data(); - - ERR_FAIL_COND(!image.is_valid()); - - image = image->duplicate(); - - if (atlas_texture.is_valid()) - image->crop_from_point( - atlas_rect.position.x, - atlas_rect.position.y, - texture_size.width, - texture_size.height); - - if (image->get_format() != Image::FORMAT_RGBA8) { - image->convert(Image::FORMAT_RGBA8); - } - - png_image png_meta; - memset(&png_meta, 0, sizeof png_meta); - png_meta.version = PNG_IMAGE_VERSION; - png_meta.width = texture_size.width; - png_meta.height = texture_size.height; - png_meta.format = PNG_FORMAT_RGBA; - - PackedByteArray png; - size_t len; - PackedByteArray data = image->get_data(); - ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); - - png.resize(len); - ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); - - char *object_url; - /* clang-format off */ - EM_ASM({ - var PNG_PTR = $0; - var PNG_LEN = $1; - var PTR = $2; - - var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: 'image/png' }); - var url = URL.createObjectURL(png); - var length_bytes = lengthBytesUTF8(url) + 1; - var string_on_wasm_heap = _malloc(length_bytes); - setValue(PTR, string_on_wasm_heap, '*'); - stringToUTF8(url, string_on_wasm_heap, length_bytes); - }, png.ptr(), len, &object_url); - /* clang-format on */ - - String url = String::utf8(object_url) + "?" + itos(p_hotspot.x) + " " + itos(p_hotspot.y); - - /* clang-format off */ - EM_ASM({ _free($0); }, object_url); - /* clang-format on */ - - if (cursors[p_shape] != "") { - /* clang-format off */ - EM_ASM({ - URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); - }, cursors[p_shape].utf8().get_data()); - /* clang-format on */ - cursors[p_shape] = ""; - } - - cursors[p_shape] = url; - - Vector<Variant> params; - params.push_back(p_cursor); - params.push_back(p_hotspot); - cursors_cache.insert(p_shape, params); - - } else if (cursors[p_shape] != "") { - /* clang-format off */ - EM_ASM({ - URL.revokeObjectURL(UTF8ToString($0).split('?')[0]); - }, cursors[p_shape].utf8().get_data()); - /* clang-format on */ - cursors[p_shape] = ""; - - cursors_cache.erase(p_shape); - } - - set_cursor_shape(cursor_shape); -} - -void OS_JavaScript::set_mouse_mode(OS::MouseMode p_mode) { - - ERR_FAIL_COND_MSG(p_mode == MOUSE_MODE_CONFINED, "MOUSE_MODE_CONFINED is not supported for the HTML5 platform."); - if (p_mode == get_mouse_mode()) - return; - - if (p_mode == MOUSE_MODE_VISIBLE) { - - // set_css_cursor must be called before set_cursor_shape to make the cursor visible - set_css_cursor(godot2dom_cursor(cursor_shape)); - set_cursor_shape(cursor_shape); - emscripten_exit_pointerlock(); - - } else if (p_mode == MOUSE_MODE_HIDDEN) { - - set_css_cursor("none"); - emscripten_exit_pointerlock(); - - } else if (p_mode == MOUSE_MODE_CAPTURED) { - - EMSCRIPTEN_RESULT result = emscripten_request_pointerlock("canvas", false); - ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); - ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "MOUSE_MODE_CAPTURED can only be entered from within an appropriate input callback."); - // set_css_cursor must be called before set_cursor_shape to make the cursor visible - set_css_cursor(godot2dom_cursor(cursor_shape)); - set_cursor_shape(cursor_shape); - } -} - -OS::MouseMode OS_JavaScript::get_mouse_mode() const { - - if (is_css_cursor_hidden()) - return MOUSE_MODE_HIDDEN; - - EmscriptenPointerlockChangeEvent ev; - emscripten_get_pointerlock_status(&ev); - return (ev.isActive && String::utf8(ev.id) == "canvas") ? MOUSE_MODE_CAPTURED : MOUSE_MODE_VISIBLE; -} - -// Wheel - -EM_BOOL OS_JavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data) { - - ERR_FAIL_COND_V(p_event_type != EMSCRIPTEN_EVENT_WHEEL, false); - if (!is_canvas_focused()) { - if (cursor_inside_canvas) { - focus_canvas(); - } else { - return false; - } - } - - InputDefault *input = get_singleton()->input; - Ref<InputEventMouseButton> ev; - ev.instance(); - ev->set_position(input->get_mouse_position()); - ev->set_global_position(ev->get_position()); - - ev->set_shift(input->is_key_pressed(KEY_SHIFT)); - ev->set_alt(input->is_key_pressed(KEY_ALT)); - ev->set_control(input->is_key_pressed(KEY_CONTROL)); - ev->set_metakey(input->is_key_pressed(KEY_META)); - - if (p_event->deltaY < 0) - ev->set_button_index(BUTTON_WHEEL_UP); - else if (p_event->deltaY > 0) - ev->set_button_index(BUTTON_WHEEL_DOWN); - else if (p_event->deltaX > 0) - ev->set_button_index(BUTTON_WHEEL_LEFT); - else if (p_event->deltaX < 0) - ev->set_button_index(BUTTON_WHEEL_RIGHT); - else - return false; - - // Different browsers give wildly different delta values, and we can't - // interpret deltaMode, so use default value for wheel events' factor. - - int button_flag = 1 << (ev->get_button_index() - 1); - - ev->set_pressed(true); - ev->set_button_mask(input->get_mouse_button_mask() | button_flag); - input->parse_input_event(ev); - - ev->set_pressed(false); - ev->set_button_mask(input->get_mouse_button_mask() & ~button_flag); - input->parse_input_event(ev); - - return true; -} - -// Touch - bool OS_JavaScript::has_touchscreen_ui_hint() const { /* clang-format off */ @@ -726,134 +49,6 @@ bool OS_JavaScript::has_touchscreen_ui_hint() const { /* clang-format on */ } -EM_BOOL OS_JavaScript::touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - Ref<InputEventScreenTouch> ev; - ev.instance(); - int lowest_id_index = -1; - for (int i = 0; i < p_event->numTouches; ++i) { - - const EmscriptenTouchPoint &touch = p_event->touches[i]; - if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) - lowest_id_index = i; - if (!touch.isChanged) - continue; - ev->set_index(touch.identifier); - ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); - os->touches[i] = ev->get_position(); - ev->set_pressed(p_event_type == EMSCRIPTEN_EVENT_TOUCHSTART); - - os->input->parse_input_event(ev); - } - // Resume audio context after input in case autoplay was denied. - os->audio_driver_javascript.resume(); - return true; -} - -EM_BOOL OS_JavaScript::touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data) { - - OS_JavaScript *os = get_singleton(); - Ref<InputEventScreenDrag> ev; - ev.instance(); - int lowest_id_index = -1; - for (int i = 0; i < p_event->numTouches; ++i) { - - const EmscriptenTouchPoint &touch = p_event->touches[i]; - if (lowest_id_index == -1 || touch.identifier < p_event->touches[lowest_id_index].identifier) - lowest_id_index = i; - if (!touch.isChanged) - continue; - ev->set_index(touch.identifier); - ev->set_position(compute_position_in_canvas(touch.clientX, touch.clientY)); - Point2 &prev = os->touches[i]; - ev->set_relative(ev->get_position() - prev); - prev = ev->get_position(); - - os->input->parse_input_event(ev); - } - return true; -} - -// Gamepad - -EM_BOOL OS_JavaScript::gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data) { - - InputDefault *input = get_singleton()->input; - if (p_event_type == EMSCRIPTEN_EVENT_GAMEPADCONNECTED) { - - String guid = ""; - if (String::utf8(p_event->mapping) == "standard") - guid = "Default HTML5 Gamepad"; - input->joy_connection_changed(p_event->index, true, String::utf8(p_event->id), guid); - } else { - input->joy_connection_changed(p_event->index, false, ""); - } - return true; -} - -void OS_JavaScript::process_joypads() { - - int joypad_count = emscripten_get_num_gamepads(); - for (int joypad = 0; joypad < joypad_count; joypad++) { - EmscriptenGamepadEvent state; - EMSCRIPTEN_RESULT query_result = emscripten_get_gamepad_status(joypad, &state); - // Chromium reserves gamepads slots, so NO_DATA is an expected result. - ERR_CONTINUE(query_result != EMSCRIPTEN_RESULT_SUCCESS && - query_result != EMSCRIPTEN_RESULT_NO_DATA); - if (query_result == EMSCRIPTEN_RESULT_SUCCESS && state.connected) { - - int button_count = MIN(state.numButtons, 18); - int axis_count = MIN(state.numAxes, 8); - for (int button = 0; button < button_count; button++) { - - float value = state.analogButton[button]; - if (String::utf8(state.mapping) == "standard" && (button == JOY_ANALOG_L2 || button == JOY_ANALOG_R2)) { - InputDefault::JoyAxis joy_axis; - joy_axis.min = 0; - joy_axis.value = value; - input->joy_axis(joypad, button, joy_axis); - } else { - input->joy_button(joypad, button, value); - } - } - for (int axis = 0; axis < axis_count; axis++) { - - InputDefault::JoyAxis joy_axis; - joy_axis.min = -1; - joy_axis.value = state.axis[axis]; - input->joy_axis(joypad, axis, joy_axis); - } - } - } -} - -bool OS_JavaScript::is_joy_known(int p_device) { - - return input->is_joy_mapped(p_device); -} - -String OS_JavaScript::get_joy_guid(int p_device) const { - - return input->get_joy_guid_remapped(p_device); -} - -// Video - -int OS_JavaScript::get_video_driver_count() const { - - return VIDEO_DRIVER_MAX; -} - -const char *OS_JavaScript::get_video_driver_name(int p_driver) const { - - switch (p_driver) { - case VIDEO_DRIVER_GLES2: - return "GLES2"; - } - ERR_FAIL_V_MSG(nullptr, "Invalid video driver index: " + itos(p_driver) + "."); -} - // Audio int OS_JavaScript::get_audio_driver_count() const { @@ -866,192 +61,29 @@ const char *OS_JavaScript::get_audio_driver_name(int p_driver) const { return "JavaScript"; } -// Clipboard -extern "C" EMSCRIPTEN_KEEPALIVE void update_clipboard(const char *p_text) { - // Only call set_clipboard from OS (sets local clipboard) - OS::get_singleton()->OS::set_clipboard(p_text); -} - -void OS_JavaScript::set_clipboard(const String &p_text) { - OS::set_clipboard(p_text); - /* clang-format off */ - int err = EM_ASM_INT({ - var text = UTF8ToString($0); - if (!navigator.clipboard || !navigator.clipboard.writeText) - return 1; - navigator.clipboard.writeText(text).catch(function(e) { - // Setting OS clipboard is only possible from an input callback. - console.error("Setting OS clipboard is only possible from an input callback for the HTML5 plafrom. Exception:", e); - }); - return 0; - }, p_text.utf8().get_data()); - /* clang-format on */ - ERR_FAIL_COND_MSG(err, "Clipboard API is not supported."); -} - -String OS_JavaScript::get_clipboard() const { - /* clang-format off */ - EM_ASM({ - try { - navigator.clipboard.readText().then(function (result) { - ccall('update_clipboard', 'void', ['string'], [result]); - }).catch(function (e) { - // Fail graciously. - }); - } catch (e) { - // Fail graciously. - } - }); - /* clang-format on */ - return this->OS::get_clipboard(); -} - // Lifecycle -int OS_JavaScript::get_current_video_driver() const { - return video_driver_index; -} - -void OS_JavaScript::initialize_core() { +void OS_JavaScript::initialize() { OS_Unix::initialize_core(); FileAccess::make_default<FileAccessBufferedFA<FileAccessUnix>>(FileAccess::ACCESS_RESOURCES); -} - -Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { - -#if 0 - EmscriptenWebGLContextAttributes attributes; - emscripten_webgl_init_context_attributes(&attributes); - attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed"); - attributes.antialias = false; - ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER); - - if (p_desired.layered) { - set_window_per_pixel_transparency_enabled(true); - } - - bool gl_initialization_error = false; - - if (RasterizerGLES2::is_viable() == OK) { - attributes.majorVersion = 1; - RasterizerGLES2::register_config(); - RasterizerGLES2::make_current(); - } else { - gl_initialization_error = true; - } - - EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(GODOT_CANVAS_SELECTOR, &attributes); - if (emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS) { - gl_initialization_error = true; - } - - if (gl_initialization_error) { - OS::get_singleton()->alert("Your browser does not seem to support WebGL. Please update your browser version.", - "Unable to initialize video driver"); - return ERR_UNAVAILABLE; - } - - video_driver_index = p_video_driver; - - video_mode = p_desired; - // fullscreen_change_callback will correct this if the request is successful. - video_mode.fullscreen = false; - // Emscripten only attempts fullscreen requests if the user input callback - // was registered through one its own functions, so request manually for - // start-up fullscreen. - if (p_desired.fullscreen) { - /* clang-format off */ - EM_ASM({ - const canvas = Module.canvas; - (canvas.requestFullscreen || canvas.msRequestFullscreen || - canvas.mozRequestFullScreen || canvas.mozRequestFullscreen || - canvas.webkitRequestFullscreen - ).call(canvas); - }); - /* clang-format on */ - } - /* clang-format off */ - if (EM_ASM_INT_V({ return Module.resizeCanvasOnStart })) { - /* clang-format on */ - set_window_size(Size2(video_mode.width, video_mode.height)); - } else { - set_window_size(get_window_size()); - } -#endif - RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? + DisplayServerJavaScript::register_javascript_driver(); char locale_ptr[16]; /* clang-format off */ - EM_ASM_ARGS({ - stringToUTF8(Module.locale, $0, 16); + EM_ASM({ + stringToUTF8(Module['locale'], $0, 16); }, locale_ptr); /* clang-format on */ setenv("LANG", locale_ptr, true); +} - AudioDriverManager::initialize(p_audio_driver); - RenderingServer *rendering_server = memnew(RenderingServerRaster()); - input = memnew(InputDefault); - - EMSCRIPTEN_RESULT result; -#define EM_CHECK(ev) \ - if (result != EMSCRIPTEN_RESULT_SUCCESS) \ - ERR_PRINT("Error while setting " #ev " callback: Code " + itos(result)); -#define SET_EM_CALLBACK(target, ev, cb) \ - result = emscripten_set_##ev##_callback(target, nullptr, true, &cb); \ - EM_CHECK(ev) -#define SET_EM_CALLBACK_NOTARGET(ev, cb) \ - result = emscripten_set_##ev##_callback(nullptr, true, &cb); \ - EM_CHECK(ev) - // These callbacks from Emscripten's html5.h suffice to access most - // JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM - // is used below. - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, mousedown, mouse_button_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, wheel, wheel_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchstart, touch_press_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchmove, touchmove_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchend, touch_press_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchcancel, touch_press_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keydown, keydown_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keypress, keypress_callback) - SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keyup, keyup_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback) - SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback) - SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback) -#undef SET_EM_CALLBACK_NOTARGET -#undef SET_EM_CALLBACK -#undef EM_CHECK - - /* clang-format off */ - EM_ASM_ARGS({ - const send_notification = cwrap('send_notification', null, ['number']); - const notifications = arguments; - (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { - Module.canvas.addEventListener(event, send_notification.bind(null, notifications[index])); - }); - // Clipboard - const update_clipboard = cwrap('update_clipboard', null, ['string']); - window.addEventListener('paste', function(evt) { - update_clipboard(evt.clipboardData.getData('text')); - }, true); - }, - NOTIFICATION_WM_MOUSE_ENTER, - NOTIFICATION_WM_MOUSE_EXIT, - NOTIFICATION_WM_FOCUS_IN, - NOTIFICATION_WM_FOCUS_OUT - ); - /* clang-format on */ - - rendering_server->init(); - - return OK; +void OS_JavaScript::resume_audio() { + audio_driver_javascript.resume(); } void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) { main_loop = p_main_loop; - input->set_main_loop(p_main_loop); } MainLoop *OS_JavaScript::get_main_loop() const { @@ -1059,12 +91,6 @@ MainLoop *OS_JavaScript::get_main_loop() const { return main_loop; } -void OS_JavaScript::run_async() { - - main_loop->init(); - emscripten_set_main_loop(main_loop_callback, -1, false); -} - void OS_JavaScript::main_loop_callback() { get_singleton()->main_loop_iterate(); @@ -1090,50 +116,51 @@ bool OS_JavaScript::main_loop_iterate() { } } - if (emscripten_sample_gamepad_data() == EMSCRIPTEN_RESULT_SUCCESS) - process_joypads(); - - if (just_exited_fullscreen) { - if (window_maximized) { - EmscriptenFullscreenStrategy strategy; - strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; - strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; - strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = nullptr; - emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy); - } else { - emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, windowed_size.width, windowed_size.height); - } - just_exited_fullscreen = false; - } - - int canvas[2]; - emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, canvas, canvas + 1); - video_mode.width = canvas[0]; - video_mode.height = canvas[1]; - if (!window_maximized && !video_mode.fullscreen && !just_exited_fullscreen && !entering_fullscreen) { - windowed_size.width = canvas[0]; - windowed_size.height = canvas[1]; - } + DisplayServer::get_singleton()->process_events(); return Main::iteration(); } void OS_JavaScript::delete_main_loop() { - memdelete(main_loop); + if (main_loop) { + memdelete(main_loop); + } + main_loop = nullptr; +} + +void OS_JavaScript::finalize_async() { + finalizing = true; + audio_driver_javascript.finish_async(); } void OS_JavaScript::finalize() { - memdelete(input); + delete_main_loop(); } // Miscellaneous Error OS_JavaScript::execute(const String &p_path, const List<String> &p_arguments, bool p_blocking, ProcessID *r_child_id, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex) { - ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "OS::execute() is not available on the HTML5 platform."); + Array args; + for (const List<String>::Element *E = p_arguments.front(); E; E = E->next()) { + args.push_back(E->get()); + } + String json_args = JSON::print(args); + /* clang-format off */ + int failed = EM_ASM_INT({ + const json_args = UTF8ToString($0); + const args = JSON.parse(json_args); + if (Module["onExecute"]) { + Module["onExecute"](args); + return 0; + } + return 1; + }, json_args.utf8().get_data()); + /* clang-format on */ + ERR_FAIL_COND_V_MSG(failed, ERR_UNAVAILABLE, "OS::execute() must be implemented in JavaScript via 'engine.setOnExecute' if required."); + return OK; } Error OS_JavaScript::kill(const ProcessID &p_pid) { @@ -1146,14 +173,6 @@ int OS_JavaScript::get_process_id() const { ERR_FAIL_V_MSG(0, "OS::get_process_id() is not available on the HTML5 platform."); } -extern "C" EMSCRIPTEN_KEEPALIVE void send_notification(int p_notification) { - - if (p_notification == NOTIFICATION_WM_MOUSE_ENTER || p_notification == NOTIFICATION_WM_MOUSE_EXIT) { - cursor_inside_canvas = p_notification == NOTIFICATION_WM_MOUSE_ENTER; - } - OS_JavaScript::get_singleton()->get_main_loop()->notification(p_notification); -} - bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) { if (p_feature == "HTML5" || p_feature == "web") @@ -1167,72 +186,6 @@ bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) { return false; } -void OS_JavaScript::alert(const String &p_alert, const String &p_title) { - - /* clang-format off */ - EM_ASM_({ - window.alert(UTF8ToString($0)); - }, p_alert.utf8().get_data()); - /* clang-format on */ -} - -void OS_JavaScript::set_window_title(const String &p_title) { - - /* clang-format off */ - EM_ASM_({ - document.title = UTF8ToString($0); - }, p_title.utf8().get_data()); - /* clang-format on */ -} - -void OS_JavaScript::set_icon(const Ref<Image> &p_icon) { - - ERR_FAIL_COND(p_icon.is_null()); - Ref<Image> icon = p_icon; - if (icon->is_compressed()) { - icon = icon->duplicate(); - ERR_FAIL_COND(icon->decompress() != OK); - } - if (icon->get_format() != Image::FORMAT_RGBA8) { - if (icon == p_icon) - icon = icon->duplicate(); - icon->convert(Image::FORMAT_RGBA8); - } - - png_image png_meta; - memset(&png_meta, 0, sizeof png_meta); - png_meta.version = PNG_IMAGE_VERSION; - png_meta.width = icon->get_width(); - png_meta.height = icon->get_height(); - png_meta.format = PNG_FORMAT_RGBA; - - PackedByteArray png; - size_t len; - PackedByteArray data = icon->get_data(); - ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); - - png.resize(len); - ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); - - /* clang-format off */ - EM_ASM_ARGS({ - var PNG_PTR = $0; - var PNG_LEN = $1; - - var png = new Blob([HEAPU8.slice(PNG_PTR, PNG_PTR + PNG_LEN)], { type: "image/png" }); - var url = URL.createObjectURL(png); - var link = document.getElementById('-gd-engine-icon'); - if (link === null) { - link = document.createElement('link'); - link.rel = 'icon'; - link.id = '-gd-engine-icon'; - document.head.appendChild(link); - } - link.href = url; - }, png.ptr(), len); - /* clang-format on */ -} - String OS_JavaScript::get_executable_path() const { return OS::get_executable_path(); @@ -1264,9 +217,19 @@ String OS_JavaScript::get_user_data_dir() const { return "/userfs"; }; -String OS_JavaScript::get_resource_dir() const { +String OS_JavaScript::get_cache_path() const { + + return "/home/web_user/.cache"; +} + +String OS_JavaScript::get_config_path() const { - return "/"; + return "/home/web_user/.config"; +} + +String OS_JavaScript::get_data_path() const { + + return "/home/web_user/.local/share"; } void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) { @@ -1294,27 +257,10 @@ OS_JavaScript *OS_JavaScript::get_singleton() { return static_cast<OS_JavaScript *>(OS::get_singleton()); } -OS_JavaScript::OS_JavaScript(int p_argc, char *p_argv[]) { - - List<String> arguments; - for (int i = 1; i < p_argc; i++) { - arguments.push_back(String::utf8(p_argv[i])); - } - set_cmdline(p_argv[0], arguments); - - last_click_button_index = -1; - last_click_ms = 0; - last_click_pos = Point2(-100, -100); - - window_maximized = false; - entering_fullscreen = false; - just_exited_fullscreen = false; - transparency_enabled = false; - - main_loop = nullptr; +void OS_JavaScript::initialize_joypads() { +} - idb_available = false; - sync_wait_time = -1; +OS_JavaScript::OS_JavaScript() { AudioDriverManager::add_driver(&audio_driver_javascript); diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index feb0f0651a..0a81a4a5b3 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -35,64 +35,25 @@ #include "core/input/input.h" #include "drivers/unix/os_unix.h" #include "servers/audio_server.h" -#include "servers/rendering/rasterizer.h" #include <emscripten/html5.h> class OS_JavaScript : public OS_Unix { - VideoMode video_mode; - Vector2 windowed_size; - bool window_maximized; - bool entering_fullscreen; - bool just_exited_fullscreen; - bool transparency_enabled; - - InputDefault *input; - Ref<InputEventKey> deferred_key_event; - CursorShape cursor_shape; - String cursors[CURSOR_MAX]; - Map<CursorShape, Vector<Variant>> cursors_cache; - Point2 touches[32]; - - Point2i last_click_pos; - double last_click_ms; - int last_click_button_index; - - MainLoop *main_loop; - int video_driver_index; + MainLoop *main_loop = nullptr; AudioDriverJavaScript audio_driver_javascript; - bool idb_available; - int64_t sync_wait_time; - int64_t last_sync_check_time; - - static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data); - - static EM_BOOL keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); - static EM_BOOL keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); - static EM_BOOL keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data); - - static EM_BOOL mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); - static EM_BOOL mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data); - - static EM_BOOL wheel_callback(int p_event_type, const EmscriptenWheelEvent *p_event, void *p_user_data); - - static EM_BOOL touch_press_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); - static EM_BOOL touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); - - static EM_BOOL gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data); - void process_joypads(); + bool finalizing = false; + bool idb_available = false; + int64_t sync_wait_time = -1; + int64_t last_sync_check_time = -1; static void main_loop_callback(); static void file_access_close_callback(const String &p_file, int p_flags); protected: - virtual int get_current_video_driver() const; - - virtual void initialize_core(); - virtual Error initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver); + virtual void initialize(); virtual void set_main_loop(MainLoop *p_main_loop); virtual void delete_main_loop(); @@ -105,65 +66,38 @@ public: // Override return type to make writing static callbacks less tedious. static OS_JavaScript *get_singleton(); - virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0); - virtual VideoMode get_video_mode(int p_screen = 0) const; - virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const; - - virtual void set_window_size(const Size2); - virtual Size2 get_window_size() const; - virtual void set_window_maximized(bool p_enabled); - virtual bool is_window_maximized() const; - virtual void set_window_fullscreen(bool p_enabled); - virtual bool is_window_fullscreen() const; - virtual Size2 get_screen_size(int p_screen = -1) const; - - virtual Point2 get_mouse_position() const; - virtual int get_mouse_button_state() const; - virtual void set_cursor_shape(CursorShape p_shape); - virtual void set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot); - virtual void set_mouse_mode(MouseMode p_mode); - virtual MouseMode get_mouse_mode() const; - - virtual bool get_window_per_pixel_transparency_enabled() const; - virtual void set_window_per_pixel_transparency_enabled(bool p_enabled); + virtual void initialize_joypads(); virtual bool has_touchscreen_ui_hint() const; - virtual bool is_joy_known(int p_device); - virtual String get_joy_guid(int p_device) const; - - virtual int get_video_driver_count() const; - virtual const char *get_video_driver_name(int p_driver) const; - virtual int get_audio_driver_count() const; virtual const char *get_audio_driver_name(int p_driver) const; - virtual void set_clipboard(const String &p_text); - virtual String get_clipboard() const; - virtual MainLoop *get_main_loop() const; - void run_async(); + void finalize_async(); bool main_loop_iterate(); virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr); virtual Error kill(const ProcessID &p_pid); virtual int get_process_id() const; - virtual void alert(const String &p_alert, const String &p_title = "ALERT!"); - virtual void set_window_title(const String &p_title); - virtual void set_icon(const Ref<Image> &p_icon); String get_executable_path() const; virtual Error shell_open(String p_uri); virtual String get_name() const; virtual bool can_draw() const; - virtual String get_resource_dir() const; + virtual String get_cache_path() const; + virtual String get_config_path() const; + virtual String get_data_path() const; virtual String get_user_data_dir() const; void set_idb_available(bool p_idb_available); virtual bool is_userfs_persistent() const; - OS_JavaScript(int p_argc, char *p_argv[]); + void resume_audio(); + bool is_finalizing() { return finalizing; } + + OS_JavaScript(); }; #endif diff --git a/scene/2d/visibility_notifier_2d.cpp b/scene/2d/visibility_notifier_2d.cpp index c374dd5faa..780d08693d 100644 --- a/scene/2d/visibility_notifier_2d.cpp +++ b/scene/2d/visibility_notifier_2d.cpp @@ -248,10 +248,19 @@ void VisibilityEnabler2D::_notification(int p_what) { _find_nodes(from); - if (enabler[ENABLER_PARENT_PHYSICS_PROCESS] && get_parent()) - get_parent()->set_physics_process(false); - if (enabler[ENABLER_PARENT_PROCESS] && get_parent()) - get_parent()->set_process(false); + // We need to defer the call of set_process and set_physics_process, + // otherwise they are overwritten inside NOTIFICATION_READY. + // We can't use call_deferred, because it happens after a physics frame. + // The ready signal works as it's emitted immediately after NOTIFICATION_READY. + + if (enabler[ENABLER_PARENT_PHYSICS_PROCESS] && get_parent()) { + get_parent()->connect(SceneStringNames::get_singleton()->ready, + callable_mp(get_parent(), &Node::set_physics_process), varray(false), CONNECT_ONESHOT); + } + if (enabler[ENABLER_PARENT_PROCESS] && get_parent()) { + get_parent()->connect(SceneStringNames::get_singleton()->ready, + callable_mp(get_parent(), &Node::set_process), varray(false), CONNECT_ONESHOT); + } } if (p_what == NOTIFICATION_EXIT_TREE) { diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 8574b05016..bb177ae0e7 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -277,10 +277,14 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { } break; #ifdef APPLE_STYLE_KEYS case (KEY_LEFT): { // Go to start of text - like HOME key. + shift_selection_check_pre(k->get_shift()); set_cursor_position(0); + shift_selection_check_post(k->get_shift()); } break; case (KEY_RIGHT): { // Go to end of text - like END key. + shift_selection_check_pre(k->get_shift()); set_cursor_position(text.length()); + shift_selection_check_post(k->get_shift()); } break; #endif default: { |