diff options
56 files changed, 730 insertions, 176 deletions
diff --git a/core/doc_data.h b/core/doc_data.h index 04bd55eaba..6a7f4355db 100644 --- a/core/doc_data.h +++ b/core/doc_data.h @@ -522,6 +522,10 @@ public: String type; String data_type; String description; + bool is_deprecated = false; + String deprecated_message; + bool is_experimental = false; + String experimental_message; String default_value; String keywords; bool operator<(const ThemeItemDoc &p_theme_item) const { @@ -550,6 +554,16 @@ public: doc.description = p_dict["description"]; } + if (p_dict.has("deprecated")) { + doc.is_deprecated = true; + doc.deprecated_message = p_dict["deprecated"]; + } + + if (p_dict.has("experimental")) { + doc.is_experimental = true; + doc.experimental_message = p_dict["experimental"]; + } + if (p_dict.has("default_value")) { doc.default_value = p_dict["default_value"]; } @@ -579,6 +593,14 @@ public: dict["description"] = p_doc.description; } + if (p_doc.is_deprecated) { + dict["deprecated"] = p_doc.deprecated_message; + } + + if (p_doc.is_experimental) { + dict["experimental"] = p_doc.experimental_message; + } + if (!p_doc.default_value.is_empty()) { dict["default_value"] = p_doc.default_value; } diff --git a/doc/class.xsd b/doc/class.xsd index f0e1241053..28e02e870d 100644 --- a/doc/class.xsd +++ b/doc/class.xsd @@ -246,6 +246,8 @@ <xs:attribute type="xs:string" name="data_type" /> <xs:attribute type="xs:string" name="type" /> <xs:attribute type="xs:string" name="default" use="optional" /> + <xs:attribute type="xs:string" name="deprecated" use="optional" /> + <xs:attribute type="xs:string" name="experimental" use="optional" /> <xs:attribute type="xs:string" name="keywords" use="optional" /> </xs:extension> </xs:simpleContent> diff --git a/doc/classes/CanvasItem.xml b/doc/classes/CanvasItem.xml index 186ee1b9c4..0a0223c550 100644 --- a/doc/classes/CanvasItem.xml +++ b/doc/classes/CanvasItem.xml @@ -96,6 +96,7 @@ <param index="3" name="texture" type="Texture2D" default="null" /> <description> Draws a colored polygon of any number of points, convex or concave. Unlike [method draw_polygon], a single color must be specified for the whole polygon. + [b]Note:[/b] If you frequently redraw the same polygon with a large number of vertices, consider pre-calculating the triangulation with [method Geometry2D.triangulate_polygon] and using [method draw_mesh], [method draw_multimesh], or [method RenderingServer.canvas_item_add_triangle_array]. </description> </method> <method name="draw_dashed_line"> @@ -251,6 +252,7 @@ <param index="3" name="texture" type="Texture2D" default="null" /> <description> Draws a solid polygon of any number of points, convex or concave. Unlike [method draw_colored_polygon], each point's color can be changed individually. See also [method draw_polyline] and [method draw_polyline_colors]. If you need more flexibility (such as being able to use bones), use [method RenderingServer.canvas_item_add_triangle_array] instead. + [b]Note:[/b] If you frequently redraw the same polygon with a large number of vertices, consider pre-calculating the triangulation with [method Geometry2D.triangulate_polygon] and using [method draw_mesh], [method draw_multimesh], or [method RenderingServer.canvas_item_add_triangle_array]. </description> </method> <method name="draw_polyline"> diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index d2118f1942..7f017f39de 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -970,6 +970,7 @@ - [b]Auto (based on screen size)[/b] (default) will automatically choose how to launch the Play window based on the device and screen metrics. Defaults to [b]Same as Editor[/b] on phones and [b]Side-by-side with Editor[/b] on tablets. - [b]Same as Editor[/b] will launch the Play window in the same window as the Editor. - [b]Side-by-side with Editor[/b] will launch the Play window side-by-side with the Editor window. + - [b]Launch in PiP mode[/b] will launch the Play window directly in picture-in-picture (PiP) mode if PiP mode is supported and enabled. When maximized, the Play window will occupy the same window as the Editor. [b]Note:[/b] Only available in the Android editor. </member> <member name="run/window_placement/play_window_pip_mode" type="int" setter="" getter=""> diff --git a/doc/classes/MeshInstance3D.xml b/doc/classes/MeshInstance3D.xml index abbb4c4eeb..d8e2c43566 100644 --- a/doc/classes/MeshInstance3D.xml +++ b/doc/classes/MeshInstance3D.xml @@ -21,6 +21,14 @@ [b]Performance:[/b] [Mesh] data needs to be received from the GPU, stalling the [RenderingServer] in the process. </description> </method> + <method name="bake_mesh_from_current_skeleton_pose"> + <return type="ArrayMesh" /> + <param index="0" name="existing" type="ArrayMesh" default="null" /> + <description> + Takes a snapshot of the current animated skeleton pose of the skinned mesh and bakes it to the provided [param existing] mesh. If no [param existing] mesh is provided a new [ArrayMesh] is created, baked, and returned. Requires a skeleton with a registered skin to work. Blendshapes are ignored. Mesh surface materials are not copied. + [b]Performance:[/b] [Mesh] data needs to be retrieved from the GPU, stalling the [RenderingServer] in the process. + </description> + </method> <method name="create_convex_collision"> <return type="void" /> <param index="0" name="clean" type="bool" default="true" /> diff --git a/doc/classes/PathFollow2D.xml b/doc/classes/PathFollow2D.xml index 61db34fb02..7444bfc23a 100644 --- a/doc/classes/PathFollow2D.xml +++ b/doc/classes/PathFollow2D.xml @@ -26,6 +26,7 @@ </member> <member name="progress_ratio" type="float" setter="set_progress_ratio" getter="get_progress_ratio" default="0.0"> The distance along the path as a number in the range 0.0 (for the first vertex) to 1.0 (for the last). This is just another way of expressing the progress within the path, as the offset supplied is multiplied internally by the path's length. + It can be set or get only if the [PathFollow2D] is the child of a [Path2D] which is part of the scene tree, and that this [Path2D] has a [Curve2D] with a non-zero length. Otherwise, trying to set this field will print an error, and getting this field will return [code]0.0[/code]. </member> <member name="rotates" type="bool" setter="set_rotates" getter="is_rotating" default="true"> If [code]true[/code], this node rotates to follow the path, with the +X direction facing forward on the path. diff --git a/doc/classes/PathFollow3D.xml b/doc/classes/PathFollow3D.xml index b505c6ea8b..bcd04d51ca 100644 --- a/doc/classes/PathFollow3D.xml +++ b/doc/classes/PathFollow3D.xml @@ -36,6 +36,7 @@ </member> <member name="progress_ratio" type="float" setter="set_progress_ratio" getter="get_progress_ratio" default="0.0"> The distance from the first vertex, considering 0.0 as the first vertex and 1.0 as the last. This is just another way of expressing the progress within the path, as the progress supplied is multiplied internally by the path's length. + It can be set or get only if the [PathFollow3D] is the child of a [Path3D] which is part of the scene tree, and that this [Path3D] has a [Curve3D] with a non-zero length. Otherwise, trying to set this field will print an error, and getting this field will return [code]0.0[/code]. </member> <member name="rotation_mode" type="int" setter="set_rotation_mode" getter="get_rotation_mode" enum="PathFollow3D.RotationMode" default="3"> Allows or forbids rotation on one or more axes, depending on the [enum RotationMode] constants being used. diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index 4cdfba17e9..cea9a4cec4 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -329,6 +329,7 @@ <param index="4" name="texture" type="RID" default="RID()" /> <description> Draws a 2D polygon on the [CanvasItem] pointed to by the [param item] [RID]. If you need more flexibility (such as being able to use bones), use [method canvas_item_add_triangle_array] instead. See also [method CanvasItem.draw_polygon]. + [b]Note:[/b] If you frequently redraw the same polygon with a large number of vertices, consider pre-calculating the triangulation with [method Geometry2D.triangulate_polygon] and using [method CanvasItem.draw_mesh], [method CanvasItem.draw_multimesh], or [method canvas_item_add_triangle_array]. </description> </method> <method name="canvas_item_add_polyline"> diff --git a/doc/tools/make_rst.py b/doc/tools/make_rst.py index 761a7f8f4a..29e10ea490 100755 --- a/doc/tools/make_rst.py +++ b/doc/tools/make_rst.py @@ -1311,9 +1311,6 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir: if property_def.text is not None and property_def.text.strip() != "": f.write(f"{format_text_block(property_def.text.strip(), property_def, state)}\n\n") - if property_def.type_name.type_name in PACKED_ARRAY_TYPES: - tmp = f"[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [{property_def.type_name.type_name}] for more details." - f.write(f"{format_text_block(tmp, property_def, state)}\n\n") elif property_def.deprecated is None and property_def.experimental is None: f.write(".. container:: contribute\n\n\t") f.write( @@ -1323,6 +1320,11 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir: + "\n\n" ) + # Add copy note to built-in properties returning `Packed*Array`. + if property_def.type_name.type_name in PACKED_ARRAY_TYPES: + copy_note = f"[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [{property_def.type_name.type_name}] for more details." + f.write(f"{format_text_block(copy_note, property_def, state)}\n\n") + index += 1 # Constructor, Method, Operator descriptions diff --git a/drivers/gles3/rasterizer_canvas_gles3.cpp b/drivers/gles3/rasterizer_canvas_gles3.cpp index b9206f310e..df7aba265b 100644 --- a/drivers/gles3/rasterizer_canvas_gles3.cpp +++ b/drivers/gles3/rasterizer_canvas_gles3.cpp @@ -468,7 +468,7 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_ update_skeletons = false; } // Canvas group begins here, render until before this item - _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, false, r_render_info); + _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, false, r_render_info, material_screen_texture_mipmaps_cached); item_count = 0; if (ci->canvas_group_owner->canvas_group->mode != RS::CANVAS_GROUP_MODE_TRANSPARENT) { @@ -499,7 +499,7 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_ mesh_storage->update_mesh_instances(); update_skeletons = false; } - _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, true, r_render_info); + _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, true, r_render_info, material_screen_texture_mipmaps_cached); item_count = 0; if (ci->canvas_group->blur_mipmaps) { @@ -523,7 +523,7 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_ } //render anything pending, including clearing if no items - _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, false, r_render_info); + _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, false, r_render_info, material_screen_texture_mipmaps_cached); item_count = 0; texture_storage->render_target_copy_to_back_buffer(p_to_render_target, back_buffer_rect, backbuffer_gen_mipmaps); @@ -553,7 +553,7 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_ mesh_storage->update_mesh_instances(); update_skeletons = false; } - _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, canvas_group_owner != nullptr, r_render_info); + _render_items(p_to_render_target, item_count, canvas_transform_inverse, p_light_list, r_sdf_used, canvas_group_owner != nullptr, r_render_info, material_screen_texture_mipmaps_cached); //then reset item_count = 0; } @@ -573,10 +573,10 @@ void RasterizerCanvasGLES3::canvas_render_items(RID p_to_render_target, Item *p_ state.current_instance_buffer_index = 0; } -void RasterizerCanvasGLES3::_render_items(RID p_to_render_target, int p_item_count, const Transform2D &p_canvas_transform_inverse, Light *p_lights, bool &r_sdf_used, bool p_to_backbuffer, RenderingMethod::RenderInfo *r_render_info) { +void RasterizerCanvasGLES3::_render_items(RID p_to_render_target, int p_item_count, const Transform2D &p_canvas_transform_inverse, Light *p_lights, bool &r_sdf_used, bool p_to_backbuffer, RenderingMethod::RenderInfo *r_render_info, bool p_backbuffer_has_mipmaps) { GLES3::MaterialStorage *material_storage = GLES3::MaterialStorage::get_singleton(); - canvas_begin(p_to_render_target, p_to_backbuffer); + canvas_begin(p_to_render_target, p_to_backbuffer, p_backbuffer_has_mipmaps); if (p_item_count <= 0) { // Nothing to draw, just call canvas_begin() to clear the render target and return. @@ -2169,7 +2169,7 @@ bool RasterizerCanvasGLES3::free(RID p_rid) { void RasterizerCanvasGLES3::update() { } -void RasterizerCanvasGLES3::canvas_begin(RID p_to_render_target, bool p_to_backbuffer) { +void RasterizerCanvasGLES3::canvas_begin(RID p_to_render_target, bool p_to_backbuffer, bool p_backbuffer_has_mipmaps) { GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton(); GLES3::Config *config = GLES3::Config::get_singleton(); @@ -2184,6 +2184,7 @@ void RasterizerCanvasGLES3::canvas_begin(RID p_to_render_target, bool p_to_backb glBindFramebuffer(GL_FRAMEBUFFER, render_target->fbo); glActiveTexture(GL_TEXTURE0 + config->max_texture_image_units - 4); glBindTexture(GL_TEXTURE_2D, render_target->backbuffer); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, p_backbuffer_has_mipmaps ? render_target->mipmap_count - 1 : 0); } if (render_target->is_transparent || p_to_backbuffer) { diff --git a/drivers/gles3/rasterizer_canvas_gles3.h b/drivers/gles3/rasterizer_canvas_gles3.h index 027f717eb7..9c0d0abccb 100644 --- a/drivers/gles3/rasterizer_canvas_gles3.h +++ b/drivers/gles3/rasterizer_canvas_gles3.h @@ -335,7 +335,7 @@ public: typedef void Texture; - void canvas_begin(RID p_to_render_target, bool p_to_backbuffer); + void canvas_begin(RID p_to_render_target, bool p_to_backbuffer, bool p_backbuffer_has_mipmaps); //virtual void draw_window_margins(int *black_margin, RID *black_image) override; void draw_lens_distortion_rect(const Rect2 &p_rect, float p_k1, float p_k2, const Vector2 &p_eye_center, float p_oversample); @@ -361,7 +361,7 @@ public: void _prepare_canvas_texture(RID p_texture, RS::CanvasItemTextureFilter p_base_filter, RS::CanvasItemTextureRepeat p_base_repeat, uint32_t &r_index, Size2 &r_texpixel_size); void canvas_render_items(RID p_to_render_target, Item *p_item_list, const Color &p_modulate, Light *p_light_list, Light *p_directional_list, const Transform2D &p_canvas_transform, RS::CanvasItemTextureFilter p_default_filter, RS::CanvasItemTextureRepeat p_default_repeat, bool p_snap_2d_vertices_to_pixel, bool &r_sdf_used, RenderingMethod::RenderInfo *r_render_info = nullptr) override; - void _render_items(RID p_to_render_target, int p_item_count, const Transform2D &p_canvas_transform_inverse, Light *p_lights, bool &r_sdf_used, bool p_to_backbuffer = false, RenderingMethod::RenderInfo *r_render_info = nullptr); + void _render_items(RID p_to_render_target, int p_item_count, const Transform2D &p_canvas_transform_inverse, Light *p_lights, bool &r_sdf_used, bool p_to_backbuffer = false, RenderingMethod::RenderInfo *r_render_info = nullptr, bool p_backbuffer_has_mipmaps = false); void _record_item_commands(const Item *p_item, RID p_render_target, const Transform2D &p_canvas_transform_inverse, Item *¤t_clip, GLES3::CanvasShaderData::BlendMode p_blend_mode, Light *p_lights, uint32_t &r_index, bool &r_break_batch, bool &r_sdf_used, const Point2 &p_repeat_offset); void _render_batch(Light *p_lights, uint32_t p_index, RenderingMethod::RenderInfo *r_render_info = nullptr); bool _bind_material(GLES3::CanvasMaterialData *p_material_data, CanvasShaderGLES3::ShaderVariant p_variant, uint64_t p_specialization); diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp index 3ed8042f3f..8b6d3e3268 100644 --- a/drivers/gles3/rasterizer_scene_gles3.cpp +++ b/drivers/gles3/rasterizer_scene_gles3.cpp @@ -912,7 +912,7 @@ void RasterizerSceneGLES3::_update_sky_radiance(RID p_env, const Projection &p_p RS::SkyMode sky_mode = sky->mode; if (sky_mode == RS::SKY_MODE_AUTOMATIC) { - if (shader_data->uses_time || shader_data->uses_position) { + if ((shader_data->uses_time || shader_data->uses_position) && sky->radiance_size == 256) { update_single_frame = true; sky_mode = RS::SKY_MODE_REALTIME; } else if (shader_data->uses_light || shader_data->ubo_size > 0) { diff --git a/editor/animation_bezier_editor.cpp b/editor/animation_bezier_editor.cpp index feb5f3d20d..24fcfd3930 100644 --- a/editor/animation_bezier_editor.cpp +++ b/editor/animation_bezier_editor.cpp @@ -174,7 +174,7 @@ void AnimationBezierTrackEdit::_draw_track(int p_track, const Color &p_color) { } if (lines.size() >= 2) { - draw_multiline(lines, p_color, Math::round(EDSCALE)); + draw_multiline(lines, p_color, Math::round(EDSCALE), true); } } } @@ -208,7 +208,7 @@ void AnimationBezierTrackEdit::_draw_line_clipped(const Vector2 &p_from, const V from = from.lerp(to, c); } - draw_line(from, to, p_color, Math::round(EDSCALE)); + draw_line(from, to, p_color, Math::round(EDSCALE), true); } void AnimationBezierTrackEdit::_notification(int p_what) { diff --git a/editor/doc_tools.cpp b/editor/doc_tools.cpp index bf5b717c19..76934c9ec6 100644 --- a/editor/doc_tools.cpp +++ b/editor/doc_tools.cpp @@ -277,6 +277,10 @@ static void merge_theme_properties(Vector<DocData::ThemeItemDoc> &p_to, const Ve // Check found entry on name and data type. if (to.name == from.name && to.data_type == from.data_type) { to.description = from.description; + to.is_deprecated = from.is_deprecated; + to.deprecated_message = from.deprecated_message; + to.is_experimental = from.is_experimental; + to.experimental_message = from.experimental_message; to.keywords = from.keywords; } } @@ -1420,6 +1424,14 @@ Error DocTools::_load(Ref<XMLParser> parser) { prop2.type = parser->get_named_attribute_value("type"); ERR_FAIL_COND_V(!parser->has_attribute("data_type"), ERR_FILE_CORRUPT); prop2.data_type = parser->get_named_attribute_value("data_type"); + if (parser->has_attribute("deprecated")) { + prop2.is_deprecated = true; + prop2.deprecated_message = parser->get_named_attribute_value("deprecated"); + } + if (parser->has_attribute("experimental")) { + prop2.is_experimental = true; + prop2.experimental_message = parser->get_named_attribute_value("experimental"); + } if (parser->has_attribute("keywords")) { prop2.keywords = parser->get_named_attribute_value("keywords"); } @@ -1738,6 +1750,12 @@ Error DocTools::save_classes(const String &p_default_path, const HashMap<String, if (!ti.default_value.is_empty()) { additional_attributes += String(" default=\"") + ti.default_value.xml_escape(true) + "\""; } + if (ti.is_deprecated) { + additional_attributes += " deprecated=\"" + ti.deprecated_message.xml_escape(true) + "\""; + } + if (ti.is_experimental) { + additional_attributes += " experimental=\"" + ti.experimental_message.xml_escape(true) + "\""; + } if (!ti.keywords.is_empty()) { additional_attributes += String(" keywords=\"") + ti.keywords.xml_escape(true) + "\""; } diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp index 3d3caf59eb..e13a984213 100644 --- a/editor/editor_file_system.cpp +++ b/editor/editor_file_system.cpp @@ -410,11 +410,13 @@ void EditorFileSystem::_scan_filesystem() { new_filesystem->parent = nullptr; ScannedDirectory *sd; + HashSet<String> *processed_files = nullptr; // On the first scan, the first_scan_root_dir is created in _first_scan_filesystem. if (first_scan) { sd = first_scan_root_dir; // Will be updated on scan. ResourceUID::get_singleton()->clear(); + processed_files = memnew(HashSet<String>()); } else { Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES); sd = memnew(ScannedDirectory); @@ -422,14 +424,18 @@ void EditorFileSystem::_scan_filesystem() { nb_files_total = _scan_new_dir(sd, d); } - _process_file_system(sd, new_filesystem, sp); + _process_file_system(sd, new_filesystem, sp, processed_files); + if (first_scan) { + _process_removed_files(*processed_files); + } dep_update_list.clear(); file_cache.clear(); //clear caches, no longer needed if (first_scan) { memdelete(first_scan_root_dir); first_scan_root_dir = nullptr; + memdelete(processed_files); } else { //on the first scan this is done from the main thread after re-importing _save_filesystem_cache(); @@ -990,7 +996,7 @@ int EditorFileSystem::_scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da) return nb_files_total_scan; } -void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress) { +void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress, HashSet<String> *r_processed_files) { p_dir->modified_time = FileAccess::get_modified_time(p_scan_dir->full_path); for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) { @@ -998,7 +1004,7 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, sub_dir->parent = p_dir; sub_dir->name = scan_sub_dir->name; p_dir->subdirs.push_back(sub_dir); - _process_file_system(scan_sub_dir, sub_dir, p_progress); + _process_file_system(scan_sub_dir, sub_dir, p_progress, r_processed_files); } for (const String &scan_file : p_scan_dir->files) { @@ -1014,6 +1020,10 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, fi->file = scan_file; p_dir->files.push_back(fi); + if (r_processed_files) { + r_processed_files->insert(path); + } + FileCache *fc = file_cache.getptr(path); uint64_t mt = FileAccess::get_modified_time(path); @@ -1139,6 +1149,20 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, } } +void EditorFileSystem::_process_removed_files(const HashSet<String> &p_processed_files) { + for (const KeyValue<String, EditorFileSystem::FileCache> &kv : file_cache) { + if (!p_processed_files.has(kv.key)) { + if (ClassDB::is_parent_class(kv.value.type, SNAME("Script"))) { + // A script has been removed from disk since the last startup. The documentation needs to be updated. + // There's no need to add the path in update_script_paths since that is exclusively for updating global class names, + // which is handled in _first_scan_filesystem before the full scan to ensure plugins and autoloads can be created. + MutexLock update_script_lock(update_script_mutex); + update_script_paths_documentation.insert(kv.key); + } + } + } +} + void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanProgress &p_progress) { uint64_t current_mtime = FileAccess::get_modified_time(p_dir->get_path()); @@ -1206,7 +1230,7 @@ void EditorFileSystem::_scan_fs_changes(EditorFileSystemDirectory *p_dir, ScanPr int nb_files_dir = _scan_new_dir(&sd, d); p_progress.hi += nb_files_dir; diff_nb_files += nb_files_dir; - _process_file_system(&sd, efd, p_progress); + _process_file_system(&sd, efd, p_progress, nullptr); ItemAction ia; ia.action = ItemAction::ACTION_DIR_ADD; @@ -1872,7 +1896,12 @@ void EditorFileSystem::_update_script_classes() { EditorProgress *ep = nullptr; if (update_script_paths.size() > 1) { - ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size())); + if (MessageQueue::get_singleton()->is_flushing()) { + // Use background progress when message queue is flushing. + ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size(), false, true)); + } else { + ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size())); + } } int step_count = 0; @@ -1911,7 +1940,12 @@ void EditorFileSystem::_update_script_documentation() { EditorProgress *ep = nullptr; if (update_script_paths_documentation.size() > 1) { - ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size())); + if (MessageQueue::get_singleton()->is_flushing()) { + // Use background progress when message queue is flushing. + ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size(), false, true)); + } else { + ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size())); + } } int step_count = 0; diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h index 2ceb3fe4a5..7848ede8a7 100644 --- a/editor/editor_file_system.h +++ b/editor/editor_file_system.h @@ -239,7 +239,7 @@ class EditorFileSystem : public Node { HashSet<String> import_extensions; int _scan_new_dir(ScannedDirectory *p_dir, Ref<DirAccess> &da); - void _process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress); + void _process_file_system(const ScannedDirectory *p_scan_dir, EditorFileSystemDirectory *p_dir, ScanProgress &p_progress, HashSet<String> *p_processed_files); Thread thread_sources; bool scanning_changes = false; @@ -287,6 +287,7 @@ class EditorFileSystem : public Node { void _update_script_classes(); void _update_script_documentation(); void _process_update_pending(); + void _process_removed_files(const HashSet<String> &p_processed_files); Mutex update_scene_mutex; HashSet<String> update_scene_paths; diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index 9b0c05d910..8bba3493e5 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -1456,10 +1456,31 @@ void EditorHelp::_update_doc() { _push_normal_font(); class_desc->push_color(theme_cache.comment_color); + bool has_prev_text = false; + + if (theme_item.is_deprecated) { + has_prev_text = true; + DEPRECATED_DOC_MSG(HANDLE_DOC(theme_item.deprecated_message), TTR("This theme property may be changed or removed in future versions.")); + } + + if (theme_item.is_experimental) { + if (has_prev_text) { + class_desc->add_newline(); + class_desc->add_newline(); + } + has_prev_text = true; + EXPERIMENTAL_DOC_MSG(HANDLE_DOC(theme_item.experimental_message), TTR("This theme property may be changed or removed in future versions.")); + } + const String descr = HANDLE_DOC(theme_item.description); if (!descr.is_empty()) { + if (has_prev_text) { + class_desc->add_newline(); + class_desc->add_newline(); + } + has_prev_text = true; _add_text(descr); - } else { + } else if (!has_prev_text) { class_desc->add_image(get_editor_theme_icon(SNAME("Error"))); class_desc->add_text(" "); class_desc->push_color(theme_cache.comment_color); @@ -2220,12 +2241,6 @@ void EditorHelp::_update_doc() { } has_prev_text = true; _add_text(descr); - // Add copy note to built-in properties returning Packed*Array. - if (!cd.is_script_doc && packed_array_types.has(prop.type)) { - class_desc->add_newline(); - class_desc->add_newline(); - _add_text(vformat(TTR("[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [%s] for more details."), prop.type)); - } } else if (!has_prev_text) { class_desc->add_image(get_editor_theme_icon(SNAME("Error"))); class_desc->add_text(" "); @@ -2238,6 +2253,13 @@ void EditorHelp::_update_doc() { class_desc->pop(); // color } + // Add copy note to built-in properties returning `Packed*Array`. + if (!cd.is_script_doc && packed_array_types.has(prop.type)) { + class_desc->add_newline(); + class_desc->add_newline(); + _add_text(vformat(TTR("[b]Note:[/b] The returned array is [i]copied[/i] and any changes to it will not update the original property value. See [%s] for more details."), prop.type)); + } + class_desc->pop(); // color _pop_normal_font(); class_desc->pop(); // indent @@ -3380,6 +3402,20 @@ EditorHelpBit::HelpData EditorHelpBit::_get_theme_item_help_data(const StringNam for (const DocData::ThemeItemDoc &theme_item : E->value.theme_properties) { HelpData current; current.description = HANDLE_DOC(theme_item.description); + if (theme_item.is_deprecated) { + if (theme_item.deprecated_message.is_empty()) { + current.deprecated_message = TTR("This theme property may be changed or removed in future versions."); + } else { + current.deprecated_message = HANDLE_DOC(theme_item.deprecated_message); + } + } + if (theme_item.is_experimental) { + if (theme_item.experimental_message.is_empty()) { + current.experimental_message = TTR("This theme property may be changed or removed in future versions."); + } else { + current.experimental_message = HANDLE_DOC(theme_item.experimental_message); + } + } if (theme_item.name == p_theme_item_name) { result = current; diff --git a/editor/editor_help_search.cpp b/editor/editor_help_search.cpp index eb97337b37..987c7f9c31 100644 --- a/editor/editor_help_search.cpp +++ b/editor/editor_help_search.cpp @@ -1248,7 +1248,7 @@ TreeItem *EditorHelpSearch::Runner::_create_property_item(TreeItem *p_parent, co TreeItem *EditorHelpSearch::Runner::_create_theme_property_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::ThemeItemDoc> &p_match) { String tooltip = p_match.doc->type + " " + p_class_doc->name + "." + p_match.doc->name; tooltip += _build_keywords_tooltip(p_match.doc->keywords); - return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberTheme"), p_match.doc->name, p_match.doc->name, TTRC("Theme Property"), "theme_item", p_match.doc->keywords, tooltip, false, false, p_match.name ? String() : p_match.keyword); + return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberTheme"), p_match.doc->name, p_match.doc->name, TTRC("Theme Property"), "theme_item", p_match.doc->keywords, tooltip, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); } TreeItem *EditorHelpSearch::Runner::_create_member_item(TreeItem *p_parent, const String &p_class_name, const StringName &p_icon, const String &p_name, const String &p_text, const String &p_type, const String &p_metatype, const String &p_tooltip, const String &p_keywords, bool p_is_deprecated, bool p_is_experimental, const String &p_matching_keyword) { diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 47b5fa4d1a..f154cbd1e2 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -175,7 +175,7 @@ static const String REMOVE_ANDROID_BUILD_TEMPLATE_MESSAGE = "The Android build t static const String INSTALL_ANDROID_BUILD_TEMPLATE_MESSAGE = "This will set up your project for gradle Android builds by installing the source template to \"%s\".\nNote that in order to make gradle builds instead of using pre-built APKs, the \"Use Gradle Build\" option should be enabled in the Android export preset."; bool EditorProgress::step(const String &p_state, int p_step, bool p_force_refresh) { - if (Thread::is_main_thread()) { + if (!force_background && Thread::is_main_thread()) { return EditorNode::progress_task_step(task, p_state, p_step, p_force_refresh); } else { EditorNode::progress_task_step_bg(task, p_step); @@ -183,17 +183,18 @@ bool EditorProgress::step(const String &p_state, int p_step, bool p_force_refres } } -EditorProgress::EditorProgress(const String &p_task, const String &p_label, int p_amount, bool p_can_cancel) { - if (Thread::is_main_thread()) { +EditorProgress::EditorProgress(const String &p_task, const String &p_label, int p_amount, bool p_can_cancel, bool p_force_background) { + if (!p_force_background && Thread::is_main_thread()) { EditorNode::progress_add_task(p_task, p_label, p_amount, p_can_cancel); } else { EditorNode::progress_add_task_bg(p_task, p_label, p_amount); } task = p_task; + force_background = p_force_background; } EditorProgress::~EditorProgress() { - if (Thread::is_main_thread()) { + if (!force_background && Thread::is_main_thread()) { EditorNode::progress_end_task(task); } else { EditorNode::progress_end_task_bg(task); diff --git a/editor/editor_node.h b/editor/editor_node.h index bdf9b26a7a..e24bae73f0 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -124,9 +124,10 @@ class WindowWrapper; struct EditorProgress { String task; + bool force_background = false; bool step(const String &p_state, int p_step = -1, bool p_force_refresh = true); - EditorProgress(const String &p_task, const String &p_label, int p_amount, bool p_can_cancel = false); + EditorProgress(const String &p_task, const String &p_label, int p_amount, bool p_can_cancel = false, bool p_force_background = false); ~EditorProgress(); }; diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 644916e83f..36fbd91313 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -824,7 +824,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/screen", -5, screen_hints) #endif // Should match the ANDROID_WINDOW_* constants in 'platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt' - String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2"; + String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2,Launch in PiP mode:3"; EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints) int default_play_window_pip_mode = 0; diff --git a/editor/gui/editor_file_dialog.cpp b/editor/gui/editor_file_dialog.cpp index 7aa19509e1..b6aa3c2215 100644 --- a/editor/gui/editor_file_dialog.cpp +++ b/editor/gui/editor_file_dialog.cpp @@ -115,6 +115,8 @@ void EditorFileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_file file_name = ProjectSettings::get_singleton()->localize_path(file_name); } } + selected_options = p_selected_options; + String f = files[0]; if (mode == FILE_MODE_OPEN_FILES) { emit_signal(SNAME("files_selected"), files); @@ -146,7 +148,6 @@ void EditorFileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_file } file->set_text(f); dir->set_text(f.get_base_dir()); - selected_options = p_selected_options; filter->select(p_filter); } diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp index 52ba98b4d5..2de22238f4 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -174,15 +174,40 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i EditorDockManager::get_singleton()->focus_dock(NodeDock::get_singleton()); NodeDock::get_singleton()->show_groups(); } else if (p_id == BUTTON_UNIQUE) { - undo_redo->create_action(TTR("Disable Scene Unique Name")); - undo_redo->add_do_method(n, "set_unique_name_in_owner", false); - undo_redo->add_undo_method(n, "set_unique_name_in_owner", true); - undo_redo->add_do_method(this, "_update_tree"); - undo_redo->add_undo_method(this, "_update_tree"); - undo_redo->commit_action(); + bool ask_before_revoking_unique_name = EDITOR_GET("docks/scene_tree/ask_before_revoking_unique_name"); + revoke_node = n; + if (ask_before_revoking_unique_name) { + String msg = vformat(TTR("Revoke unique name for node \"%s\"?"), n->get_name()); + ask_before_revoke_checkbox->set_pressed(false); + revoke_dialog_label->set_text(msg); + revoke_dialog->reset_size(); + revoke_dialog->popup_centered(); + } else { + _revoke_unique_name(); + } } } +void SceneTreeEditor::_update_ask_before_revoking_unique_name() { + if (ask_before_revoke_checkbox->is_pressed()) { + EditorSettings::get_singleton()->set("docks/scene_tree/ask_before_revoking_unique_name", false); + ask_before_revoke_checkbox->set_pressed(false); + } + + _revoke_unique_name(); +} + +void SceneTreeEditor::_revoke_unique_name() { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + undo_redo->create_action(TTR("Disable Scene Unique Name")); + undo_redo->add_do_method(revoke_node, "set_unique_name_in_owner", false); + undo_redo->add_undo_method(revoke_node, "set_unique_name_in_owner", true); + undo_redo->add_do_method(this, "_update_tree"); + undo_redo->add_undo_method(this, "_update_tree"); + undo_redo->commit_action(); +} + void SceneTreeEditor::_toggle_visible(Node *p_node) { if (p_node->has_method("is_visible") && p_node->has_method("set_visible")) { bool v = bool(p_node->call("is_visible")); @@ -1620,6 +1645,18 @@ SceneTreeEditor::SceneTreeEditor(bool p_label, bool p_can_rename, bool p_can_ope update_node_tooltip_delay->set_one_shot(true); add_child(update_node_tooltip_delay); + revoke_dialog = memnew(ConfirmationDialog); + revoke_dialog->set_ok_button_text(TTR("Revoke")); + add_child(revoke_dialog); + revoke_dialog->connect(SceneStringName(confirmed), callable_mp(this, &SceneTreeEditor::_update_ask_before_revoking_unique_name)); + VBoxContainer *vb = memnew(VBoxContainer); + revoke_dialog->add_child(vb); + revoke_dialog_label = memnew(Label); + vb->add_child(revoke_dialog_label); + ask_before_revoke_checkbox = memnew(CheckBox(TTR("Don't Ask Again"))); + ask_before_revoke_checkbox->set_tooltip_text(TTR("This dialog can also be enabled/disabled in the Editor Settings: Docks > Scene Tree > Ask Before Revoking Unique Name.")); + vb->add_child(ask_before_revoke_checkbox); + script_types = memnew(List<StringName>); ClassDB::get_inheriters_from_class("Script", script_types); } diff --git a/editor/gui/scene_tree_editor.h b/editor/gui/scene_tree_editor.h index b4d9644f16..9633993b8b 100644 --- a/editor/gui/scene_tree_editor.h +++ b/editor/gui/scene_tree_editor.h @@ -31,6 +31,7 @@ #ifndef SCENE_TREE_EDITOR_H #define SCENE_TREE_EDITOR_H +#include "scene/gui/check_box.h" #include "scene/gui/check_button.h" #include "scene/gui/dialogs.h" #include "scene/gui/tree.h" @@ -68,6 +69,11 @@ class SceneTreeEditor : public Control { AcceptDialog *error = nullptr; AcceptDialog *warning = nullptr; + ConfirmationDialog *revoke_dialog = nullptr; + Label *revoke_dialog_label = nullptr; + CheckBox *ask_before_revoke_checkbox = nullptr; + Node *revoke_node = nullptr; + bool auto_expand_selected = true; bool connect_to_script_mode = false; bool connecting_signal = false; @@ -144,6 +150,9 @@ class SceneTreeEditor : public Control { Vector<StringName> valid_types; + void _update_ask_before_revoking_unique_name(); + void _revoke_unique_name(); + public: // Public for use with callable_mp. void _update_tree(bool p_scroll_to_selected = false); diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index 054239d99f..bf8dab92f8 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -2995,8 +2995,8 @@ void VisualShaderEditor::_frame_title_popup_show(const Point2 &p_position, int p } frame_title_change_edit->set_text(node->get_title()); frame_title_change_popup->set_meta("id", p_node_id); - frame_title_change_popup->popup(); frame_title_change_popup->set_position(p_position); + frame_title_change_popup->popup(); // Select current text. frame_title_change_edit->grab_focus(); diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index baa77cb41d..9978c55d7b 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -4655,6 +4655,7 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec EDITOR_DEF("interface/editors/show_scene_tree_root_selection", true); EDITOR_DEF("interface/editors/derive_script_globals_by_name", true); EDITOR_DEF("docks/scene_tree/ask_before_deleting_related_animation_tracks", true); + EDITOR_DEF("docks/scene_tree/ask_before_revoking_unique_name", true); EDITOR_DEF("_use_favorites_root_selection", false); Resource::_update_configuration_warning = _update_configuration_warning; diff --git a/modules/betsy/image_compress_betsy.cpp b/modules/betsy/image_compress_betsy.cpp index c17651da45..bc72203b2f 100644 --- a/modules/betsy/image_compress_betsy.cpp +++ b/modules/betsy/image_compress_betsy.cpp @@ -125,6 +125,7 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) { } if (err != OK) { + compute_shader->print_errors("Betsy compress shader"); memdelete(rd); if (rcd != nullptr) { memdelete(rcd); diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index e3f2a61090..276a12f5de 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -138,7 +138,7 @@ void GDScript::_super_implicit_constructor(GDScript *p_script, GDScriptInstance } } ERR_FAIL_NULL(p_script->implicit_initializer); - if (likely(valid)) { + if (likely(p_script->valid)) { p_script->implicit_initializer->call(p_instance, nullptr, 0, r_error); } else { r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index f7eda11c8e..db7bef2f07 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -2112,7 +2112,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, // Look in blocks first. int last_assign_line = -1; const GDScriptParser::ExpressionNode *last_assigned_expression = nullptr; - GDScriptParser::DataType id_type; + GDScriptCompletionIdentifier id_type; GDScriptParser::SuiteNode *suite = p_context.current_suite; bool is_function_parameter = false; @@ -2134,7 +2134,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, if (can_be_local && suite && suite->has_local(p_identifier->name)) { const GDScriptParser::SuiteNode::Local &local = suite->get_local(p_identifier->name); - id_type = local.get_datatype(); + id_type.type = local.get_datatype(); // Check initializer as the first assignment. switch (local.type) { @@ -2172,7 +2172,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, base.type.is_meta_type = p_context.current_function && p_context.current_function->is_static; if (_guess_identifier_type_from_base(p_context, base, p_identifier->name, base_identifier)) { - id_type = base_identifier.type; + id_type = base_identifier; } } } @@ -2212,7 +2212,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, c.current_line = type_test->operand->start_line; c.current_suite = suite; if (type_test->test_datatype.is_hard_type()) { - id_type = type_test->test_datatype; + id_type.type = type_test->test_datatype; if (last_assign_line < c.current_line) { // Override last assignment. last_assign_line = c.current_line; @@ -2230,10 +2230,10 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, c.current_line = last_assign_line; GDScriptCompletionIdentifier assigned_type; if (_guess_expression_type(c, last_assigned_expression, assigned_type)) { - if (id_type.is_set() && assigned_type.type.is_set() && !GDScriptAnalyzer::check_type_compatibility(id_type, assigned_type.type)) { + if (id_type.type.is_set() && assigned_type.type.is_set() && !GDScriptAnalyzer::check_type_compatibility(id_type.type, assigned_type.type)) { // The assigned type is incompatible. The annotated type takes priority. + r_type = id_type; r_type.assigned_expression = last_assigned_expression; - r_type.type = id_type; } else { r_type = assigned_type; } @@ -2251,8 +2251,8 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, GDScriptParser::FunctionNode *parent_function = base_type.class_type->get_member(p_context.current_function->identifier->name).function; if (parent_function->parameters_indices.has(p_identifier->name)) { const GDScriptParser::ParameterNode *parameter = parent_function->parameters[parent_function->parameters_indices[p_identifier->name]]; - if ((!id_type.is_set() || id_type.is_variant()) && parameter->get_datatype().is_hard_type()) { - id_type = parameter->get_datatype(); + if ((!id_type.type.is_set() || id_type.type.is_variant()) && parameter->get_datatype().is_hard_type()) { + id_type.type = parameter->get_datatype(); } if (parameter->initializer) { GDScriptParser::CompletionContext c = p_context; @@ -2268,7 +2268,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, base_type = base_type.class_type->base_type; break; case GDScriptParser::DataType::NATIVE: { - if (id_type.is_set() && !id_type.is_variant()) { + if (id_type.type.is_set() && !id_type.type.is_variant()) { base_type = GDScriptParser::DataType(); break; } @@ -2289,8 +2289,8 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context, } } - if (id_type.is_set() && !id_type.is_variant()) { - r_type.type = id_type; + if (id_type.type.is_set() && !id_type.type.is_variant()) { + r_type = id_type; return true; } diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index b6db6a940b..92f9c5fa11 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -249,7 +249,7 @@ void GDScriptParser::override_completion_context(const Node *p_for_node, Complet if (!for_completion) { return; } - if (completion_context.node != p_for_node) { + if (p_for_node == nullptr || completion_context.node != p_for_node) { return; } CompletionContext context; @@ -264,8 +264,8 @@ void GDScriptParser::override_completion_context(const Node *p_for_node, Complet completion_context = context; } -void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node, int p_argument) { - if (!for_completion) { +void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node, int p_argument, bool p_force) { + if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) { return; } if (previous.cursor_place != GDScriptTokenizerText::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizerText::CURSOR_END && current.cursor_place == GDScriptTokenizerText::CURSOR_NONE) { @@ -283,8 +283,8 @@ void GDScriptParser::make_completion_context(CompletionType p_type, Node *p_node completion_context = context; } -void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Type p_builtin_type) { - if (!for_completion) { +void GDScriptParser::make_completion_context(CompletionType p_type, Variant::Type p_builtin_type, bool p_force) { + if (!for_completion || (!p_force && completion_context.type != COMPLETION_NONE)) { return; } if (previous.cursor_place != GDScriptTokenizerText::CURSOR_MIDDLE && previous.cursor_place != GDScriptTokenizerText::CURSOR_END && current.cursor_place == GDScriptTokenizerText::CURSOR_NONE) { @@ -2471,7 +2471,7 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_precedence(Precedence p_pr } // Completion can appear whenever an expression is expected. - make_completion_context(COMPLETION_IDENTIFIER, nullptr); + make_completion_context(COMPLETION_IDENTIFIER, nullptr, -1, false); GDScriptTokenizer::Token token = current; GDScriptTokenizer::Token::Type token_type = token.type; @@ -2488,8 +2488,17 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_precedence(Precedence p_pr advance(); // Only consume the token if there's a valid rule. + // After a token was consumed, update the completion context regardless of a previously set context. + ExpressionNode *previous_operand = (this->*prefix_rule)(nullptr, p_can_assign); +#ifdef TOOLS_ENABLED + // HACK: We can't create a context in parse_identifier since it is used in places were we don't want completion. + if (previous_operand != nullptr && previous_operand->type == GDScriptParser::Node::IDENTIFIER && prefix_rule == static_cast<ParseFunction>(&GDScriptParser::parse_identifier)) { + make_completion_context(COMPLETION_IDENTIFIER, previous_operand); + } +#endif + while (p_precedence <= get_rule(current.type)->precedence) { if (previous_operand == nullptr || (p_stop_on_assign && current.type == GDScriptTokenizer::Token::EQUAL) || lambda_ended) { return previous_operand; @@ -2924,6 +2933,11 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode } assignment->assignee = p_previous_operand; assignment->assigned_value = parse_expression(false); +#ifdef TOOLS_ENABLED + if (assignment->assigned_value != nullptr && assignment->assigned_value->type == GDScriptParser::Node::IDENTIFIER) { + override_completion_context(assignment->assigned_value, COMPLETION_ASSIGN, assignment); + } +#endif if (assignment->assigned_value == nullptr) { push_error(R"(Expected an expression after "=".)"); } diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 43c5a48fa7..2c7e730772 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1455,8 +1455,11 @@ private: } void apply_pending_warnings(); #endif - void make_completion_context(CompletionType p_type, Node *p_node, int p_argument = -1); - void make_completion_context(CompletionType p_type, Variant::Type p_builtin_type); + // Setting p_force to false will prevent the completion context from being update if a context was already set before. + // This should only be done when we push context before we consumed any tokens for the corresponding structure. + // See parse_precedence for an example. + void make_completion_context(CompletionType p_type, Node *p_node, int p_argument = -1, bool p_force = true); + void make_completion_context(CompletionType p_type, Variant::Type p_builtin_type, bool p_force = true); // In some cases it might become necessary to alter the completion context after parsing a subexpression. // For example to not override COMPLETE_CALL_ARGUMENTS with COMPLETION_NONE from string literals. void override_completion_context(const Node *p_for_node, CompletionType p_type, Node *p_node, int p_argument = -1); diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.cfg b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.cfg new file mode 100644 index 0000000000..e4759ac76b --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.cfg @@ -0,0 +1,19 @@ +[output] +include=[ + {"display": "new"}, + {"display": "SIZE_EXPAND"}, + {"display": "SIZE_EXPAND_FILL"}, + {"display": "SIZE_FILL"}, + {"display": "SIZE_SHRINK_BEGIN"}, + {"display": "SIZE_SHRINK_CENTER"}, + {"display": "SIZE_SHRINK_END"}, +] +exclude=[ + {"display": "Control.SIZE_EXPAND"}, + {"display": "Control.SIZE_EXPAND_FILL"}, + {"display": "Control.SIZE_FILL"}, + {"display": "Control.SIZE_SHRINK_BEGIN"}, + {"display": "Control.SIZE_SHRINK_CENTER"}, + {"display": "Control.SIZE_SHRINK_END"}, + {"display": "contro_var"} +] diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.gd b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.gd new file mode 100644 index 0000000000..4aeafb2e0a --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute.gd @@ -0,0 +1,7 @@ +extends Control + +var contro_var + +func _ready(): + size_flags_horizontal = Control.➡ + pass diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.cfg b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.cfg new file mode 100644 index 0000000000..e4759ac76b --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.cfg @@ -0,0 +1,19 @@ +[output] +include=[ + {"display": "new"}, + {"display": "SIZE_EXPAND"}, + {"display": "SIZE_EXPAND_FILL"}, + {"display": "SIZE_FILL"}, + {"display": "SIZE_SHRINK_BEGIN"}, + {"display": "SIZE_SHRINK_CENTER"}, + {"display": "SIZE_SHRINK_END"}, +] +exclude=[ + {"display": "Control.SIZE_EXPAND"}, + {"display": "Control.SIZE_EXPAND_FILL"}, + {"display": "Control.SIZE_FILL"}, + {"display": "Control.SIZE_SHRINK_BEGIN"}, + {"display": "Control.SIZE_SHRINK_CENTER"}, + {"display": "Control.SIZE_SHRINK_END"}, + {"display": "contro_var"} +] diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.gd b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.gd new file mode 100644 index 0000000000..47e9bd5a67 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_attribute_identifier.gd @@ -0,0 +1,7 @@ +extends Control + +var contro_var + +func _ready(): + size_flags_horizontal = Control.SIZE➡ + pass diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.cfg b/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.cfg new file mode 100644 index 0000000000..5cc4ec5fd3 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.cfg @@ -0,0 +1,12 @@ +[output] +include=[ + {"display": "Control.SIZE_EXPAND"}, + {"display": "Control.SIZE_EXPAND_FILL"}, + {"display": "Control.SIZE_FILL"}, + {"display": "Control.SIZE_SHRINK_BEGIN"}, + {"display": "Control.SIZE_SHRINK_CENTER"}, + {"display": "Control.SIZE_SHRINK_END"}, +] +exclude=[ + {"display": "contro_var"} +] diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.gd b/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.gd new file mode 100644 index 0000000000..5c96720bd3 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_identifier.gd @@ -0,0 +1,7 @@ +extends Control + +var contro_var + +func _ready(): + size_flags_horizontal = Con➡ + pass diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.cfg b/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.cfg new file mode 100644 index 0000000000..5cc4ec5fd3 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.cfg @@ -0,0 +1,12 @@ +[output] +include=[ + {"display": "Control.SIZE_EXPAND"}, + {"display": "Control.SIZE_EXPAND_FILL"}, + {"display": "Control.SIZE_FILL"}, + {"display": "Control.SIZE_SHRINK_BEGIN"}, + {"display": "Control.SIZE_SHRINK_CENTER"}, + {"display": "Control.SIZE_SHRINK_END"}, +] +exclude=[ + {"display": "contro_var"} +] diff --git a/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.gd b/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.gd new file mode 100644 index 0000000000..d8bf13e51d --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/assignment_options/enum_no_identifier.gd @@ -0,0 +1,7 @@ +extends Control + +var contro_var + +func _ready(): + size_flags_horizontal = ➡ + pass diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt index d3daa1dbbc..2e1de9a607 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt @@ -48,7 +48,12 @@ enum class LaunchPolicy { /** * Adjacent launches are enabled. */ - ADJACENT + ADJACENT, + + /** + * Launches happen in the same window but start in PiP mode. + */ + SAME_AND_LAUNCH_IN_PIP_MODE } /** diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index 1995a38c2a..405b2fb57f 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -101,6 +101,7 @@ open class GodotEditor : GodotActivity() { private const val ANDROID_WINDOW_AUTO = 0 private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1 private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2 + private const val ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE = 3 /** * Sets of constants to specify the Play window PiP mode. @@ -244,25 +245,30 @@ open class GodotEditor : GodotActivity() { val isPiPAvailable = if (editorWindowInfo.supportsPiPMode && hasPiPSystemFeature()) { val pipMode = getPlayWindowPiPMode() pipMode == PLAY_WINDOW_PIP_ENABLED || - (pipMode == PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR && launchPolicy == LaunchPolicy.SAME) + (pipMode == PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR && + (launchPolicy == LaunchPolicy.SAME || launchPolicy == LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE)) } else { false } newInstance.putExtra(EXTRA_PIP_AVAILABLE, isPiPAvailable) + var launchInPiP = false if (launchPolicy == LaunchPolicy.ADJACENT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Log.v(TAG, "Adding flag for adjacent launch") newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) } } else if (launchPolicy == LaunchPolicy.SAME) { - if (isPiPAvailable && - (updatedArgs.contains(BREAKPOINTS_ARG) || updatedArgs.contains(BREAKPOINTS_ARG_SHORT))) { - Log.v(TAG, "Launching in PiP mode because of breakpoints") - newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, true) - } + launchInPiP = isPiPAvailable && + (updatedArgs.contains(BREAKPOINTS_ARG) || updatedArgs.contains(BREAKPOINTS_ARG_SHORT)) + } else if (launchPolicy == LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE) { + launchInPiP = isPiPAvailable } + if (launchInPiP) { + Log.v(TAG, "Launching in PiP mode") + newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, launchInPiP) + } return newInstance } @@ -403,6 +409,7 @@ open class GodotEditor : GodotActivity() { when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT + ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE -> LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE else -> { // ANDROID_WINDOW_AUTO defaultLaunchPolicy diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 602ca5f52e..50ebe7077f 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -522,7 +522,7 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) { if (!item.has("name") || !item.has("values") || !item.has("default")) { continue; } - event_handler->add_option(pfdc, item["name"], item["values"], item["default_idx"]); + event_handler->add_option(pfdc, item["name"], item["values"], item["default"]); } event_handler->set_root(fd->root); @@ -625,62 +625,41 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) { } } if (fd->callback.is_valid()) { - if (fd->options_in_cb) { - Variant v_result = true; - Variant v_files = file_names; - Variant v_index = index; - Variant v_opt = options; - const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt }; - - fd->callback.call_deferredp(cb_args, 4); - } else { - Variant v_result = true; - Variant v_files = file_names; - Variant v_index = index; - const Variant *cb_args[3] = { &v_result, &v_files, &v_index }; - - fd->callback.call_deferredp(cb_args, 3); - } + MutexLock lock(ds->file_dialog_mutex); + FileDialogCallback cb; + cb.callback = fd->callback; + cb.status = true; + cb.files = file_names; + cb.index = index; + cb.options = options; + cb.opt_in_cb = fd->options_in_cb; + ds->pending_cbs.push_back(cb); } } else { if (fd->callback.is_valid()) { - if (fd->options_in_cb) { - Variant v_result = false; - Variant v_files = Vector<String>(); - Variant v_index = 0; - Variant v_opt = Dictionary(); - const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt }; - - fd->callback.call_deferredp(cb_args, 4); - } else { - Variant v_result = false; - Variant v_files = Vector<String>(); - Variant v_index = 0; - const Variant *cb_args[3] = { &v_result, &v_files, &v_index }; - - fd->callback.call_deferredp(cb_args, 3); - } + MutexLock lock(ds->file_dialog_mutex); + FileDialogCallback cb; + cb.callback = fd->callback; + cb.status = false; + cb.files = Vector<String>(); + cb.index = index; + cb.options = options; + cb.opt_in_cb = fd->options_in_cb; + ds->pending_cbs.push_back(cb); } } pfd->Release(); } else { if (fd->callback.is_valid()) { - if (fd->options_in_cb) { - Variant v_result = false; - Variant v_files = Vector<String>(); - Variant v_index = 0; - Variant v_opt = Dictionary(); - const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt }; - - fd->callback.call_deferredp(cb_args, 4); - } else { - Variant v_result = false; - Variant v_files = Vector<String>(); - Variant v_index = 0; - const Variant *cb_args[3] = { &v_result, &v_files, &v_index }; - - fd->callback.call_deferredp(cb_args, 3); - } + MutexLock lock(ds->file_dialog_mutex); + FileDialogCallback cb; + cb.callback = fd->callback; + cb.status = false; + cb.files = Vector<String>(); + cb.index = 0; + cb.options = Dictionary(); + cb.opt_in_cb = fd->options_in_cb; + ds->pending_cbs.push_back(cb); } } { @@ -768,6 +747,34 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title return OK; } +void DisplayServerWindows::process_file_dialog_callbacks() { + MutexLock lock(file_dialog_mutex); + while (!pending_cbs.is_empty()) { + FileDialogCallback cb = pending_cbs.front()->get(); + pending_cbs.pop_front(); + + if (cb.opt_in_cb) { + Variant ret; + Callable::CallError ce; + const Variant *args[4] = { &cb.status, &cb.files, &cb.index, &cb.options }; + + cb.callback.callp(args, 4, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 4, ce))); + } + } else { + Variant ret; + Callable::CallError ce; + const Variant *args[3] = { &cb.status, &cb.files, &cb.index }; + + cb.callback.callp(args, 3, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 3, ce))); + } + } + } +} + void DisplayServerWindows::mouse_set_mode(MouseMode p_mode) { _THREAD_SAFE_METHOD_ @@ -3190,6 +3197,7 @@ void DisplayServerWindows::process_events() { memdelete(E->get()); E->erase(); } + process_file_dialog_callbacks(); } void DisplayServerWindows::force_process_and_drop_events() { diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 3deb7ac8b0..54e1c9681d 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -572,6 +572,16 @@ class DisplayServerWindows : public DisplayServer { Mutex file_dialog_mutex; List<FileDialogData *> file_dialogs; HashMap<HWND, FileDialogData *> file_dialog_wnd; + struct FileDialogCallback { + Callable callback; + Variant status; + Variant files; + Variant index; + Variant options; + bool opt_in_cb = false; + }; + List<FileDialogCallback> pending_cbs; + void process_file_dialog_callbacks(); static void _thread_fd_monitor(void *p_ud); diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 47836788e1..bcc6a64671 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -1614,16 +1614,7 @@ String OS_Windows::get_executable_path() const { } bool OS_Windows::has_environment(const String &p_var) const { -#ifdef MINGW_ENABLED - return _wgetenv((LPCWSTR)(p_var.utf16().get_data())) != nullptr; -#else - WCHAR *env; - size_t len; - _wdupenv_s(&env, &len, (LPCWSTR)(p_var.utf16().get_data())); - const bool has_env = env != nullptr; - free(env); - return has_env; -#endif + return GetEnvironmentVariableW((LPCWSTR)(p_var.utf16().get_data()), nullptr, 0) > 0; } String OS_Windows::get_environment(const String &p_var) const { diff --git a/scene/2d/path_2d.cpp b/scene/2d/path_2d.cpp index 282d14da5d..c3768386e9 100644 --- a/scene/2d/path_2d.cpp +++ b/scene/2d/path_2d.cpp @@ -378,9 +378,10 @@ real_t PathFollow2D::get_progress() const { } void PathFollow2D::set_progress_ratio(real_t p_ratio) { - if (path && path->get_curve().is_valid() && path->get_curve()->get_baked_length()) { - set_progress(p_ratio * path->get_curve()->get_baked_length()); - } + ERR_FAIL_NULL_MSG(path, "Can only set progress ratio on a PathFollow2D that is the child of a Path2D which is itself part of the scene tree."); + ERR_FAIL_COND_MSG(path->get_curve().is_null(), "Can't set progress ratio on a PathFollow2D that does not have a Curve."); + ERR_FAIL_COND_MSG(!path->get_curve()->get_baked_length(), "Can't set progress ratio on a PathFollow2D that has a 0 length curve."); + set_progress(p_ratio * path->get_curve()->get_baked_length()); } real_t PathFollow2D::get_progress_ratio() const { diff --git a/scene/3d/mesh_instance_3d.cpp b/scene/3d/mesh_instance_3d.cpp index 987117a7a0..f551cb401c 100644 --- a/scene/3d/mesh_instance_3d.cpp +++ b/scene/3d/mesh_instance_3d.cpp @@ -671,6 +671,172 @@ Ref<ArrayMesh> MeshInstance3D::bake_mesh_from_current_blend_shape_mix(Ref<ArrayM return bake_mesh; } +Ref<ArrayMesh> MeshInstance3D::bake_mesh_from_current_skeleton_pose(Ref<ArrayMesh> p_existing) { + Ref<ArrayMesh> source_mesh = get_mesh(); + ERR_FAIL_COND_V_MSG(source_mesh.is_null(), Ref<ArrayMesh>(), "The source mesh must be a valid ArrayMesh."); + + Ref<ArrayMesh> bake_mesh; + + if (p_existing.is_valid()) { + ERR_FAIL_COND_V_MSG(source_mesh == p_existing, Ref<ArrayMesh>(), "The source mesh can not be the same mesh as the existing mesh."); + + bake_mesh = p_existing; + } else { + bake_mesh.instantiate(); + } + + ERR_FAIL_COND_V_MSG(skin_ref.is_null(), Ref<ArrayMesh>(), "The source mesh must have a valid skin."); + ERR_FAIL_COND_V_MSG(skin_internal.is_null(), Ref<ArrayMesh>(), "The source mesh must have a valid skin."); + RID skeleton = skin_ref->get_skeleton(); + ERR_FAIL_COND_V_MSG(!skeleton.is_valid(), Ref<ArrayMesh>(), "The source mesh must have its skin registered with a valid skeleton."); + + const int bone_count = RenderingServer::get_singleton()->skeleton_get_bone_count(skeleton); + ERR_FAIL_COND_V(bone_count <= 0, Ref<ArrayMesh>()); + ERR_FAIL_COND_V(bone_count < skin_internal->get_bind_count(), Ref<ArrayMesh>()); + + LocalVector<Transform3D> bone_transforms; + bone_transforms.resize(bone_count); + for (int bone_index = 0; bone_index < bone_count; bone_index++) { + bone_transforms[bone_index] = RenderingServer::get_singleton()->skeleton_bone_get_transform(skeleton, bone_index); + } + + bake_mesh->clear_surfaces(); + + int mesh_surface_count = source_mesh->get_surface_count(); + + for (int surface_index = 0; surface_index < mesh_surface_count; surface_index++) { + ERR_CONTINUE(source_mesh->surface_get_primitive_type(surface_index) != Mesh::PRIMITIVE_TRIANGLES); + + uint32_t surface_format = source_mesh->surface_get_format(surface_index); + + ERR_CONTINUE(0 == (surface_format & Mesh::ARRAY_FORMAT_VERTEX)); + ERR_CONTINUE(0 == (surface_format & Mesh::ARRAY_FORMAT_BONES)); + ERR_CONTINUE(0 == (surface_format & Mesh::ARRAY_FORMAT_WEIGHTS)); + + unsigned int bones_per_vertex = surface_format & Mesh::ARRAY_FLAG_USE_8_BONE_WEIGHTS ? 8 : 4; + + surface_format &= ~Mesh::ARRAY_FORMAT_BONES; + surface_format &= ~Mesh::ARRAY_FORMAT_WEIGHTS; + + const Array &source_mesh_arrays = source_mesh->surface_get_arrays(surface_index); + + ERR_FAIL_COND_V(source_mesh_arrays.size() != RS::ARRAY_MAX, Ref<ArrayMesh>()); + + const Vector<Vector3> &source_mesh_vertex_array = source_mesh_arrays[Mesh::ARRAY_VERTEX]; + const Vector<Vector3> &source_mesh_normal_array = source_mesh_arrays[Mesh::ARRAY_NORMAL]; + const Vector<float> &source_mesh_tangent_array = source_mesh_arrays[Mesh::ARRAY_TANGENT]; + const Vector<int> &source_mesh_bones_array = source_mesh_arrays[Mesh::ARRAY_BONES]; + const Vector<float> &source_mesh_weights_array = source_mesh_arrays[Mesh::ARRAY_WEIGHTS]; + + unsigned int vertex_count = source_mesh_vertex_array.size(); + int expected_bone_array_size = vertex_count * bones_per_vertex; + ERR_CONTINUE(source_mesh_bones_array.size() != expected_bone_array_size); + ERR_CONTINUE(source_mesh_weights_array.size() != expected_bone_array_size); + + Array new_mesh_arrays; + new_mesh_arrays.resize(Mesh::ARRAY_MAX); + for (int i = 0; i < source_mesh_arrays.size(); i++) { + if (i == Mesh::ARRAY_VERTEX || i == Mesh::ARRAY_NORMAL || i == Mesh::ARRAY_TANGENT || i == Mesh::ARRAY_BONES || i == Mesh::ARRAY_WEIGHTS) { + continue; + } + new_mesh_arrays[i] = source_mesh_arrays[i]; + } + + bool use_normal_array = source_mesh_normal_array.size() == source_mesh_vertex_array.size(); + bool use_tangent_array = source_mesh_tangent_array.size() / 4 == source_mesh_vertex_array.size(); + + Vector<Vector3> lerped_vertex_array = source_mesh_vertex_array; + Vector<Vector3> lerped_normal_array = source_mesh_normal_array; + Vector<float> lerped_tangent_array = source_mesh_tangent_array; + + const Vector3 *source_vertices_ptr = source_mesh_vertex_array.ptr(); + const Vector3 *source_normals_ptr = source_mesh_normal_array.ptr(); + const float *source_tangents_ptr = source_mesh_tangent_array.ptr(); + const int *source_bones_ptr = source_mesh_bones_array.ptr(); + const float *source_weights_ptr = source_mesh_weights_array.ptr(); + + Vector3 *lerped_vertices_ptrw = lerped_vertex_array.ptrw(); + Vector3 *lerped_normals_ptrw = lerped_normal_array.ptrw(); + float *lerped_tangents_ptrw = lerped_tangent_array.ptrw(); + + for (unsigned int vertex_index = 0; vertex_index < vertex_count; vertex_index++) { + Vector3 lerped_vertex; + Vector3 lerped_normal; + Vector3 lerped_tangent; + + const Vector3 &source_vertex = source_vertices_ptr[vertex_index]; + + Vector3 source_normal; + if (use_normal_array) { + source_normal = source_normals_ptr[vertex_index]; + } + + int tangent_index = vertex_index * 4; + Vector4 source_tangent; + Vector3 source_tangent_vec3; + if (use_tangent_array) { + source_tangent = Vector4( + source_tangents_ptr[tangent_index], + source_tangents_ptr[tangent_index + 1], + source_tangents_ptr[tangent_index + 2], + source_tangents_ptr[tangent_index + 3]); + + DEV_ASSERT(source_tangent.w == 1.0 || source_tangent.w == -1.0); + + source_tangent_vec3 = Vector3(source_tangent.x, source_tangent.y, source_tangent.z); + } + + for (unsigned int weight_index = 0; weight_index < bones_per_vertex; weight_index++) { + float bone_weight = source_weights_ptr[vertex_index * bones_per_vertex + weight_index]; + if (bone_weight < FLT_EPSILON) { + continue; + } + int vertex_bone_index = source_bones_ptr[vertex_index * bones_per_vertex + weight_index]; + const Transform3D &bone_transform = bone_transforms[vertex_bone_index]; + const Basis bone_basis = bone_transform.basis.orthonormalized(); + + ERR_FAIL_INDEX_V(vertex_bone_index, static_cast<int>(bone_transforms.size()), Ref<ArrayMesh>()); + + lerped_vertex += source_vertex.lerp(bone_transform.xform(source_vertex), bone_weight) - source_vertex; + ; + + if (use_normal_array) { + lerped_normal += source_normal.lerp(bone_basis.xform(source_normal), bone_weight) - source_normal; + } + + if (use_tangent_array) { + lerped_tangent += source_tangent_vec3.lerp(bone_basis.xform(source_tangent_vec3), bone_weight) - source_tangent_vec3; + } + } + + lerped_vertices_ptrw[vertex_index] += lerped_vertex; + + if (use_normal_array) { + lerped_normals_ptrw[vertex_index] = (source_normal + lerped_normal).normalized(); + } + + if (use_tangent_array) { + lerped_tangent = (source_tangent_vec3 + lerped_tangent).normalized(); + lerped_tangents_ptrw[tangent_index] = lerped_tangent.x; + lerped_tangents_ptrw[tangent_index + 1] = lerped_tangent.y; + lerped_tangents_ptrw[tangent_index + 2] = lerped_tangent.z; + } + } + + new_mesh_arrays[Mesh::ARRAY_VERTEX] = lerped_vertex_array; + if (use_normal_array) { + new_mesh_arrays[Mesh::ARRAY_NORMAL] = lerped_normal_array; + } + if (use_tangent_array) { + new_mesh_arrays[Mesh::ARRAY_TANGENT] = lerped_tangent_array; + } + + bake_mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, new_mesh_arrays, Array(), Dictionary(), surface_format); + } + + return bake_mesh; +} + void MeshInstance3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_mesh", "mesh"), &MeshInstance3D::set_mesh); ClassDB::bind_method(D_METHOD("get_mesh"), &MeshInstance3D::get_mesh); @@ -700,6 +866,7 @@ void MeshInstance3D::_bind_methods() { ClassDB::bind_method(D_METHOD("create_debug_tangents"), &MeshInstance3D::create_debug_tangents); ClassDB::bind_method(D_METHOD("bake_mesh_from_current_blend_shape_mix", "existing"), &MeshInstance3D::bake_mesh_from_current_blend_shape_mix, DEFVAL(Ref<ArrayMesh>())); + ClassDB::bind_method(D_METHOD("bake_mesh_from_current_skeleton_pose", "existing"), &MeshInstance3D::bake_mesh_from_current_skeleton_pose, DEFVAL(Ref<ArrayMesh>())); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "mesh", PROPERTY_HINT_RESOURCE_TYPE, "Mesh"), "set_mesh", "get_mesh"); ADD_GROUP("Skeleton", ""); diff --git a/scene/3d/mesh_instance_3d.h b/scene/3d/mesh_instance_3d.h index 8a7e03c5b3..0eff12762d 100644 --- a/scene/3d/mesh_instance_3d.h +++ b/scene/3d/mesh_instance_3d.h @@ -102,6 +102,7 @@ public: virtual AABB get_aabb() const override; Ref<ArrayMesh> bake_mesh_from_current_blend_shape_mix(Ref<ArrayMesh> p_existing = Ref<ArrayMesh>()); + Ref<ArrayMesh> bake_mesh_from_current_skeleton_pose(Ref<ArrayMesh> p_existing = Ref<ArrayMesh>()); MeshInstance3D(); ~MeshInstance3D(); diff --git a/scene/3d/path_3d.cpp b/scene/3d/path_3d.cpp index 1f8f7cd54c..dc030b6a0f 100644 --- a/scene/3d/path_3d.cpp +++ b/scene/3d/path_3d.cpp @@ -461,9 +461,10 @@ real_t PathFollow3D::get_progress() const { } void PathFollow3D::set_progress_ratio(real_t p_ratio) { - if (path && path->get_curve().is_valid() && path->get_curve()->get_baked_length()) { - set_progress(p_ratio * path->get_curve()->get_baked_length()); - } + ERR_FAIL_NULL_MSG(path, "Can only set progress ratio on a PathFollow3D that is the child of a Path3D which is itself part of the scene tree."); + ERR_FAIL_COND_MSG(path->get_curve().is_null(), "Can't set progress ratio on a PathFollow3D that does not have a Curve."); + ERR_FAIL_COND_MSG(!path->get_curve()->get_baked_length(), "Can't set progress ratio on a PathFollow3D that has a 0 length curve."); + set_progress(p_ratio * path->get_curve()->get_baked_length()); } real_t PathFollow3D::get_progress_ratio() const { diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index 1a1aa5ccb0..373488b0fc 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -124,6 +124,8 @@ void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int file_name = ProjectSettings::get_singleton()->localize_path(file_name); } } + selected_options = p_selected_options; + String f = files[0]; if (mode == FILE_MODE_OPEN_FILES) { emit_signal(SNAME("files_selected"), files); @@ -155,7 +157,6 @@ void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int } file->set_text(f); dir->set_text(f.get_base_dir()); - selected_options = p_selected_options; filter->select(p_filter); } diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index df668aa496..c2818edd9c 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -68,10 +68,15 @@ void LineEdit::_move_caret_left(bool p_select, bool p_move_by_word) { int cc = caret_column; PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid); - for (int i = words.size() - 2; i >= 0; i = i - 2) { - if (words[i] < cc) { - cc = words[i]; - break; + if (words.is_empty() || cc <= words[0]) { + // Move to the start when there are no more words. + cc = 0; + } else { + for (int i = words.size() - 2; i >= 0; i = i - 2) { + if (words[i] < cc) { + cc = words[i]; + break; + } } } @@ -101,10 +106,15 @@ void LineEdit::_move_caret_right(bool p_select, bool p_move_by_word) { int cc = caret_column; PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid); - for (int i = 1; i < words.size(); i = i + 2) { - if (words[i] > cc) { - cc = words[i]; - break; + if (words.is_empty() || cc >= words[words.size() - 1]) { + // Move to the end when there are no more words. + cc = text.length(); + } else { + for (int i = 1; i < words.size(); i = i + 2) { + if (words[i] > cc) { + cc = words[i]; + break; + } } } @@ -159,10 +169,15 @@ void LineEdit::_backspace(bool p_word, bool p_all_to_left) { int cc = caret_column; PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid); - for (int i = words.size() - 2; i >= 0; i = i - 2) { - if (words[i] < cc) { - cc = words[i]; - break; + if (words.is_empty() || cc <= words[0]) { + // Delete to the start when there are no more words. + cc = 0; + } else { + for (int i = words.size() - 2; i >= 0; i = i - 2) { + if (words[i] < cc) { + cc = words[i]; + break; + } } } @@ -198,10 +213,15 @@ void LineEdit::_delete(bool p_word, bool p_all_to_right) { if (p_word) { int cc = caret_column; PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid); - for (int i = 1; i < words.size(); i = i + 2) { - if (words[i] > cc) { - cc = words[i]; - break; + if (words.is_empty() || cc >= words[words.size() - 1]) { + // Delete to the end when there are no more words. + cc = text.length(); + } else { + for (int i = 1; i < words.size(); i = i + 2) { + if (words[i] > cc) { + cc = words[i]; + break; + } } } diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index a2f39af858..c4b33fd889 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -2393,7 +2393,7 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); if (words.is_empty() || cc <= words[0]) { - // This solves the scenario where there are no words but glyfs that can be ignored. + // Move to the start when there are no more words. cc = 0; } else { for (int j = words.size() - 2; j >= 0; j = j - 2) { @@ -2450,7 +2450,7 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); if (words.is_empty() || cc >= words[words.size() - 1]) { - // This solves the scenario where there are no words but glyfs that can be ignored. + // Move to the end when there are no more words. cc = text[get_caret_line(i)].length(); } else { for (int j = 1; j < words.size(); j = j + 2) { @@ -2666,7 +2666,7 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { // Get a list with the indices of the word bounds of the given text line. const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_index))->get_rid()); if (words.is_empty() || column <= words[0]) { - // If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line. + // Delete to the start when there are no more words. column = 0; } else { // Otherwise search for the first word break that is smaller than the index from we're currently deleting. @@ -2731,10 +2731,15 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { int column = get_caret_column(caret_index); PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); - for (int j = 1; j < words.size(); j = j + 2) { - if (words[j] > column) { - column = words[j]; - break; + if (words.is_empty() || column >= words[words.size() - 1]) { + // Delete to the end when there are no more words. + column = text[get_caret_line(i)].length(); + } else { + for (int j = 1; j < words.size(); j = j + 2) { + if (words[j] > column) { + column = words[j]; + break; + } } } diff --git a/servers/rendering/renderer_rd/environment/sky.cpp b/servers/rendering/renderer_rd/environment/sky.cpp index 7d4ce6888f..2087989102 100644 --- a/servers/rendering/renderer_rd/environment/sky.cpp +++ b/servers/rendering/renderer_rd/environment/sky.cpp @@ -1250,7 +1250,7 @@ void SkyRD::update_radiance_buffers(Ref<RenderSceneBuffersRD> p_render_buffers, RS::SkyMode sky_mode = sky->mode; if (sky_mode == RS::SKY_MODE_AUTOMATIC) { - if (shader_data->uses_time || shader_data->uses_position) { + if ((shader_data->uses_time || shader_data->uses_position) && sky->radiance_size == 256) { update_single_frame = true; sky_mode = RS::SKY_MODE_REALTIME; } else if (shader_data->uses_light || shader_data->ubo_size > 0) { diff --git a/tests/create_test.py b/tests/create_test.py index deb53aca20..ad6d6b882f 100755 --- a/tests/create_test.py +++ b/tests/create_test.py @@ -13,12 +13,16 @@ def main(): os.chdir(os.path.dirname(os.path.realpath(__file__))) parser = argparse.ArgumentParser(description="Creates a new unit test file.") - parser.add_argument("name", type=str, help="The unit test name in PascalCase notation") + parser.add_argument( + "name", + type=str, + help="Specifies the class or component name to be tested, in PascalCase (e.g., MeshInstance3D). The name will be prefixed with 'test_' for the header file and 'Test' for the namespace.", + ) parser.add_argument( "path", type=str, nargs="?", - help="The path to the unit test file relative to the tests folder (default: .)", + help="The path to the unit test file relative to the tests folder (e.g. core). This should correspond to the relative path of the class or component being tested. (default: .)", default=".", ) parser.add_argument( @@ -29,9 +33,10 @@ def main(): ) args = parser.parse_args() - snake_case_regex = re.compile(r"(?<!^)(?=[A-Z])") - name_snake_case = snake_case_regex.sub("_", args.name).lower() - + snake_case_regex = re.compile(r"(?<!^)(?=[A-Z, 0-9])") + # Replace 2D, 3D, and 4D with 2d, 3d, and 4d, respectively. This avoids undesired splits like node_3_d. + prefiltered_name = re.sub(r"([234])D", lambda match: match.group(1).lower() + "d", args.name) + name_snake_case = snake_case_regex.sub("_", prefiltered_name).lower() file_path = os.path.normpath(os.path.join(args.path, f"test_{name_snake_case}.h")) print(file_path) diff --git a/tests/scene/test_audio_stream_wav.h b/tests/scene/test_audio_stream_wav.h index 5166cd3c13..d3d5cc8a30 100644 --- a/tests/scene/test_audio_stream_wav.h +++ b/tests/scene/test_audio_stream_wav.h @@ -181,27 +181,27 @@ void run_test(String file_name, AudioStreamWAV::Format data_format, bool stereo, } } -TEST_CASE("[AudioStreamWAV] Mono PCM8 format") { +TEST_CASE("[Audio][AudioStreamWAV] Mono PCM8 format") { run_test("test_pcm8_mono.wav", AudioStreamWAV::FORMAT_8_BITS, false, WAV_RATE, WAV_COUNT); } -TEST_CASE("[AudioStreamWAV] Mono PCM16 format") { +TEST_CASE("[Audio][AudioStreamWAV] Mono PCM16 format") { run_test("test_pcm16_mono.wav", AudioStreamWAV::FORMAT_16_BITS, false, WAV_RATE, WAV_COUNT); } -TEST_CASE("[AudioStreamWAV] Stereo PCM8 format") { +TEST_CASE("[Audio][AudioStreamWAV] Stereo PCM8 format") { run_test("test_pcm8_stereo.wav", AudioStreamWAV::FORMAT_8_BITS, true, WAV_RATE, WAV_COUNT); } -TEST_CASE("[AudioStreamWAV] Stereo PCM16 format") { +TEST_CASE("[Audio][AudioStreamWAV] Stereo PCM16 format") { run_test("test_pcm16_stereo.wav", AudioStreamWAV::FORMAT_16_BITS, true, WAV_RATE, WAV_COUNT); } -TEST_CASE("[AudioStreamWAV] Alternate mix rate") { +TEST_CASE("[Audio][AudioStreamWAV] Alternate mix rate") { run_test("test_pcm16_stereo_38000Hz.wav", AudioStreamWAV::FORMAT_16_BITS, true, 38000, 38000); } -TEST_CASE("[AudioStreamWAV] save_to_wav() adds '.wav' file extension automatically") { +TEST_CASE("[Audio][AudioStreamWAV] save_to_wav() adds '.wav' file extension automatically") { String save_path = TestUtils::get_temp_path("test_wav_extension"); Vector<uint8_t> test_data = gen_pcm8_test(WAV_RATE, WAV_COUNT, false); Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV); @@ -213,7 +213,7 @@ TEST_CASE("[AudioStreamWAV] save_to_wav() adds '.wav' file extension automatical CHECK(error == OK); } -TEST_CASE("[AudioStreamWAV] Default values") { +TEST_CASE("[Audio][AudioStreamWAV] Default values") { Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV); CHECK(stream->get_format() == AudioStreamWAV::FORMAT_8_BITS); CHECK(stream->get_loop_mode() == AudioStreamWAV::LOOP_DISABLED); @@ -227,11 +227,11 @@ TEST_CASE("[AudioStreamWAV] Default values") { CHECK(stream->get_stream_name() == ""); } -TEST_CASE("[AudioStreamWAV] Save empty file") { +TEST_CASE("[Audio][AudioStreamWAV] Save empty file") { run_test("test_empty.wav", AudioStreamWAV::FORMAT_8_BITS, false, WAV_RATE, 0); } -TEST_CASE("[AudioStreamWAV] Saving IMA ADPCM is not supported") { +TEST_CASE("[Audio][AudioStreamWAV] Saving IMA ADPCM is not supported") { String save_path = TestUtils::get_temp_path("test_adpcm.wav"); Ref<AudioStreamWAV> stream = memnew(AudioStreamWAV); stream->set_format(AudioStreamWAV::FORMAT_IMA_ADPCM); diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 69e27fe7a0..46a5046b21 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -4232,6 +4232,18 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line(0) == 0); CHECK(text_edit->get_caret_column(0) == 4); text_edit->remove_secondary_carets(); + + // Remove when there are no words, only symbols. + text_edit->set_text("#{}"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + + SEND_GUI_ACTION("ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); } SUBCASE("[TextEdit] ui_text_backspace_word same line") { @@ -4891,6 +4903,18 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line(0) == 0); CHECK(text_edit->get_caret_column(0) == 2); text_edit->remove_secondary_carets(); + + // Remove when there are no words, only symbols. + text_edit->set_text("#{}"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + + SEND_GUI_ACTION("ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); } SUBCASE("[TextEdit] ui_text_delete_word same line") { @@ -5301,6 +5325,16 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Move when there are no words, only symbols. + text_edit->set_text("#{}"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + + SEND_GUI_ACTION("ui_text_caret_word_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); } SUBCASE("[TextEdit] ui_text_caret_left") { @@ -5563,6 +5597,16 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Move when there are no words, only symbols. + text_edit->set_text("#{}"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + + SEND_GUI_ACTION("ui_text_caret_word_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 3); } SUBCASE("[TextEdit] ui_text_caret_right") { diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 7e1c431a3c..502aed6a6e 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -320,7 +320,7 @@ struct GodotTestCaseListener : public doctest::IReporter { return; } - if (name.contains("Audio")) { + if (name.contains("[Audio]")) { // The last driver index should always be the dummy driver. int dummy_idx = AudioDriverManager::get_driver_count() - 1; AudioDriverManager::initialize(dummy_idx); |