From 48a54cb62f51cfb57a0b281d39477ba6d710cd85 Mon Sep 17 00:00:00 2001 From: Leon Stansfield Date: Sun, 4 Aug 2024 15:33:31 +0100 Subject: [PATCH 01/30] Adjusted listings of parameters displayed in inspector to be consistent accross all particle types --- scene/2d/cpu_particles_2d.cpp | 2 +- scene/2d/gpu_particles_2d.cpp | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scene/2d/cpu_particles_2d.cpp b/scene/2d/cpu_particles_2d.cpp index 9c9ba93b410..754afb05274 100644 --- a/scene/2d/cpu_particles_2d.cpp +++ b/scene/2d/cpu_particles_2d.cpp @@ -1288,7 +1288,7 @@ void CPUParticles2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "emitting"), "set_emitting", "is_emitting"); ADD_PROPERTY(PropertyInfo(Variant::INT, "amount", PROPERTY_HINT_RANGE, "1,1000000,1,exp"), "set_amount", "get_amount"); ADD_GROUP("Time", ""); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,suffix:s"), "set_lifetime", "get_lifetime"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,exp,suffix:s"), "set_lifetime", "get_lifetime"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "one_shot"), "set_one_shot", "get_one_shot"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "preprocess", PROPERTY_HINT_RANGE, "0.00,600.0,0.01,suffix:s"), "set_pre_process_time", "get_pre_process_time"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed_scale", PROPERTY_HINT_RANGE, "0,64,0.01"), "set_speed_scale", "get_speed_scale"); diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index 1d3f1ceadaf..3f9e9c8af42 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -816,19 +816,17 @@ void GPUParticles2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "amount", PROPERTY_HINT_RANGE, "1,1000000,1,exp"), "set_amount", "get_amount"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "amount_ratio", PROPERTY_HINT_RANGE, "0,1,0.0001"), "set_amount_ratio", "get_amount_ratio"); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "sub_emitter", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "GPUParticles2D"), "set_sub_emitter", "get_sub_emitter"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "process_material", PROPERTY_HINT_RESOURCE_TYPE, "ParticleProcessMaterial,ShaderMaterial"), "set_process_material", "get_process_material"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture"); ADD_GROUP("Time", ""); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,suffix:s"), "set_lifetime", "get_lifetime"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,exp,suffix:s"), "set_lifetime", "get_lifetime"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "interp_to_end", PROPERTY_HINT_RANGE, "0.00,1.0,0.001"), "set_interp_to_end", "get_interp_to_end"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "one_shot"), "set_one_shot", "get_one_shot"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "preprocess", PROPERTY_HINT_RANGE, "0.00,600.0,0.01,suffix:s"), "set_pre_process_time", "get_pre_process_time"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "preprocess", PROPERTY_HINT_RANGE, "0.00,600.0,0.01,exp,suffix:s"), "set_pre_process_time", "get_pre_process_time"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed_scale", PROPERTY_HINT_RANGE, "0,64,0.01"), "set_speed_scale", "get_speed_scale"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "explosiveness", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_explosiveness_ratio", "get_explosiveness_ratio"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "randomness", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_randomness_ratio", "get_randomness_ratio"); ADD_PROPERTY(PropertyInfo(Variant::INT, "fixed_fps", PROPERTY_HINT_RANGE, "0,1000,1,suffix:FPS"), "set_fixed_fps", "get_fixed_fps"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "interpolate"), "set_interpolate", "get_interpolate"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "fract_delta"), "set_fractional_delta", "get_fractional_delta"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "interp_to_end", PROPERTY_HINT_RANGE, "0.00,1.0,0.001"), "set_interp_to_end", "get_interp_to_end"); ADD_GROUP("Collision", "collision_"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "collision_base_size", PROPERTY_HINT_RANGE, "0,128,0.01,or_greater"), "set_collision_base_size", "get_collision_base_size"); ADD_GROUP("Drawing", ""); @@ -840,7 +838,9 @@ void GPUParticles2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "trail_lifetime", PROPERTY_HINT_RANGE, "0.01,10,0.01,or_greater,suffix:s"), "set_trail_lifetime", "get_trail_lifetime"); ADD_PROPERTY(PropertyInfo(Variant::INT, "trail_sections", PROPERTY_HINT_RANGE, "2,128,1"), "set_trail_sections", "get_trail_sections"); ADD_PROPERTY(PropertyInfo(Variant::INT, "trail_section_subdivisions", PROPERTY_HINT_RANGE, "1,1024,1"), "set_trail_section_subdivisions", "get_trail_section_subdivisions"); - + ADD_GROUP("Process Material", ""); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "process_material", PROPERTY_HINT_RESOURCE_TYPE, "ParticleProcessMaterial,ShaderMaterial"), "set_process_material", "get_process_material"); BIND_ENUM_CONSTANT(DRAW_ORDER_INDEX); BIND_ENUM_CONSTANT(DRAW_ORDER_LIFETIME); BIND_ENUM_CONSTANT(DRAW_ORDER_REVERSE_LIFETIME); @@ -874,7 +874,7 @@ GPUParticles2D::GPUParticles2D() { set_randomness_ratio(0); set_visibility_rect(Rect2(Vector2(-100, -100), Vector2(200, 200))); set_use_local_coordinates(false); - set_draw_order(DRAW_ORDER_LIFETIME); + set_draw_order(DRAW_ORDER_INDEX); set_speed_scale(1); set_fixed_fps(30); set_collision_base_size(collision_base_size); From 4aead96306c07b77c2ef28ee67a3e3c1b811d242 Mon Sep 17 00:00:00 2001 From: Leon Stansfield Date: Sun, 4 Aug 2024 15:45:48 +0100 Subject: [PATCH 02/30] Revent DRAW_ORDER_INDEX to DRAW_ORDER_LIFETIME default particle params for compatibility. --- scene/2d/gpu_particles_2d.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index 3f9e9c8af42..3a63af0ab50 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -874,7 +874,7 @@ GPUParticles2D::GPUParticles2D() { set_randomness_ratio(0); set_visibility_rect(Rect2(Vector2(-100, -100), Vector2(200, 200))); set_use_local_coordinates(false); - set_draw_order(DRAW_ORDER_INDEX); + set_draw_order(DRAW_ORDER_LIFETIME); set_speed_scale(1); set_fixed_fps(30); set_collision_base_size(collision_base_size); From 8ca7f0e33408612ddcb840005fb889de3edd4528 Mon Sep 17 00:00:00 2001 From: Hendrik Brucker Date: Fri, 30 Aug 2024 14:04:54 +0200 Subject: [PATCH 03/30] Prevent crash after removing GraphEdit's connection layer and add additional warnings --- scene/gui/graph_edit.cpp | 44 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index 60b3e371a0a..836f2277f52 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -260,6 +260,8 @@ PackedStringArray GraphEdit::get_configuration_warnings() const { } Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { + ERR_FAIL_NULL_V_MSG(connections_layer, FAILED, "connections_layer is missing."); + if (is_node_connected(p_from, p_from_port, p_to, p_to_port)) { return OK; } @@ -313,6 +315,8 @@ bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, con } void GraphEdit::disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + for (const List>::Element *E = connections.front(); E; E = E->next()) { if (E->get()->from_node == p_from && E->get()->from_port == p_from_port && E->get()->to_node == p_to && E->get()->to_port == p_to_port) { connection_map[p_from].erase(E->get()); @@ -356,6 +360,8 @@ void GraphEdit::_scroll_moved(double) { } void GraphEdit::_update_scroll_offset() { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + set_block_minimum_size_adjust(true); for (int i = 0; i < get_child_count(); i++) { @@ -524,6 +530,8 @@ void GraphEdit::_graph_element_resize_request(const Vector2 &p_new_minsize, Node } void GraphEdit::_graph_frame_autoshrink_changed(const Vector2 &p_new_minsize, GraphFrame *p_frame) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + _update_graph_frame(p_frame); minimap->queue_redraw(); @@ -535,6 +543,7 @@ void GraphEdit::_graph_frame_autoshrink_changed(const Vector2 &p_new_minsize, Gr void GraphEdit::_graph_element_moved(Node *p_node) { GraphElement *graph_element = Object::cast_to(p_node); ERR_FAIL_NULL(graph_element); + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); minimap->queue_redraw(); queue_redraw(); @@ -543,6 +552,7 @@ void GraphEdit::_graph_element_moved(Node *p_node) { } void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_node) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); GraphNode *graph_node = Object::cast_to(p_node); ERR_FAIL_NULL(graph_node); @@ -558,6 +568,8 @@ void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_node) { } void GraphEdit::_graph_node_rect_changed(GraphNode *p_node) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + // Only invalidate the cache when zooming or the node is moved/resized in graph space. if (panner->is_panning()) { return; @@ -566,7 +578,6 @@ void GraphEdit::_graph_node_rect_changed(GraphNode *p_node) { for (Ref &c : connection_map[p_node->get_name()]) { c->_cache.dirty = true; } - connections_layer->queue_redraw(); callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); @@ -623,7 +634,9 @@ void GraphEdit::add_child_notify(Node *p_child) { } graph_element->connect("raise_request", callable_mp(this, &GraphEdit::_ensure_node_order_from).bind(graph_element)); graph_element->connect("resize_request", callable_mp(this, &GraphEdit::_graph_element_resize_request).bind(graph_element)); - graph_element->connect(SceneStringName(item_rect_changed), callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw)); + if (connections_layer != nullptr && connections_layer->is_inside_tree()) { + graph_element->connect(SceneStringName(item_rect_changed), callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw)); + } graph_element->connect(SceneStringName(item_rect_changed), callable_mp((CanvasItem *)minimap, &GraphEditMinimap::queue_redraw)); graph_element->set_scale(Vector2(zoom, zoom)); @@ -640,6 +653,7 @@ void GraphEdit::remove_child_notify(Node *p_child) { minimap = nullptr; } else if (p_child == connections_layer) { connections_layer = nullptr; + WARN_PRINT("GraphEdit's connection_layer removed. This should not be done. If you like to remove all GraphElements from a GraphEdit node, do not simply remove all non-internal children but check their type since the connection layer has to be kept non-internal due to technical reasons."); } if (top_layer != nullptr && is_inside_tree()) { @@ -662,7 +676,9 @@ void GraphEdit::remove_child_notify(Node *p_child) { for (const Ref &conn : connection_map[graph_node->get_name()]) { conn->_cache.dirty = true; } - connections_layer->queue_redraw(); + if (connections_layer != nullptr && connections_layer->is_inside_tree()) { + connections_layer->queue_redraw(); + } } GraphFrame *frame = Object::cast_to(graph_element); @@ -1680,6 +1696,8 @@ void GraphEdit::set_selected(Node *p_child) { } void GraphEdit::gui_input(const Ref &p_ev) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + ERR_FAIL_COND(p_ev.is_null()); if (panner->gui_input(p_ev, warped_panning ? get_global_rect() : Rect2())) { return; @@ -2012,6 +2030,8 @@ void GraphEdit::gui_input(const Ref &p_ev) { } void GraphEdit::_pan_callback(Vector2 p_scroll_vec, Ref p_event) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + h_scrollbar->set_value(h_scrollbar->get_value() - p_scroll_vec.x); v_scrollbar->set_value(v_scrollbar->get_value() - p_scroll_vec.y); @@ -2027,6 +2047,8 @@ void GraphEdit::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref &c : connection_map[p_from]) { if (c->from_node == p_from && c->from_port == p_from_port && c->to_node == p_to && c->to_port == p_to_port) { if (!Math::is_equal_approx(c->activity, p_activity)) { @@ -2043,6 +2065,8 @@ void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_por } void GraphEdit::reset_all_connection_activity() { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + bool changed = false; for (Ref &conn : connections) { if (conn->activity > 0) { @@ -2057,6 +2081,8 @@ void GraphEdit::reset_all_connection_activity() { } void GraphEdit::clear_connections() { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + for (Ref &c : connections) { c->_cache.line->queue_free(); } @@ -2070,7 +2096,9 @@ void GraphEdit::clear_connections() { } void GraphEdit::force_connection_drag_end() { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); ERR_FAIL_COND_MSG(!connecting, "Drag end requested without active drag!"); + connecting = false; connecting_valid = false; minimap->queue_redraw(); @@ -2100,6 +2128,8 @@ void GraphEdit::set_zoom(float p_zoom) { } void GraphEdit::set_zoom_custom(float p_zoom, const Vector2 &p_center) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + p_zoom = CLAMP(p_zoom, zoom_min, zoom_max); if (zoom == p_zoom) { return; @@ -2508,6 +2538,8 @@ bool GraphEdit::is_showing_arrange_button() const { } void GraphEdit::override_connections_shader(const Ref &p_shader) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + connections_shader = p_shader; _invalidate_connection_line_cache(); @@ -2526,6 +2558,8 @@ void GraphEdit::_minimap_toggled() { } void GraphEdit::set_connection_lines_curvature(float p_curvature) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + lines_curvature = p_curvature; _invalidate_connection_line_cache(); connections_layer->queue_redraw(); @@ -2537,7 +2571,9 @@ float GraphEdit::get_connection_lines_curvature() const { } void GraphEdit::set_connection_lines_thickness(float p_thickness) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); ERR_FAIL_COND_MSG(p_thickness < 0, "Connection lines thickness must be greater than or equal to 0."); + if (lines_thickness == p_thickness) { return; } @@ -2552,6 +2588,8 @@ float GraphEdit::get_connection_lines_thickness() const { } void GraphEdit::set_connection_lines_antialiased(bool p_antialiased) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + if (lines_antialiased == p_antialiased) { return; } From 27b7b433e03114c72c89397633d82fd67e0b0b8c Mon Sep 17 00:00:00 2001 From: Marius Hanl Date: Wed, 4 Sep 2024 00:26:09 +0200 Subject: [PATCH 04/30] Fix Inspector may scrolls away when editing a property that adds or removes sub properties --- doc/classes/EditorInspector.xml | 1 + editor/editor_inspector.cpp | 24 +++++++++++++----------- editor/editor_inspector.h | 7 ++++--- editor/editor_properties.cpp | 5 ++++- editor/inspector_dock.cpp | 2 +- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/doc/classes/EditorInspector.xml b/doc/classes/EditorInspector.xml index cfdc172fd1d..6b25be490ed 100644 --- a/doc/classes/EditorInspector.xml +++ b/doc/classes/EditorInspector.xml @@ -28,6 +28,7 @@ + diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 1e5acce0329..7eb0732dbb6 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -2766,8 +2766,9 @@ void EditorInspector::update_tree() { // TODO: Can be useful to store more context for the focusable, such as the caret position in LineEdit. StringName current_selected = property_selected; int current_focusable = -1; - // Temporarily disable focus following to avoid jumping while the inspector is updating. - set_follow_focus(false); + + // Temporarily disable focus following on the root inspector to avoid jumping while the inspector is updating. + get_root_inspector()->set_follow_focus(false); if (property_focusable != -1) { // Check that focusable is actually focusable. @@ -2795,6 +2796,7 @@ void EditorInspector::update_tree() { _clear(!object); if (!object) { + get_root_inspector()->set_follow_focus(true); return; } @@ -3532,7 +3534,8 @@ void EditorInspector::update_tree() { // Updating inspector might invalidate some editing owners. EditorNode::get_singleton()->hide_unused_editors(); } - set_follow_focus(true); + + get_root_inspector()->set_follow_focus(true); } void EditorInspector::update_property(const String &p_prop) { @@ -3777,11 +3780,10 @@ void EditorInspector::set_use_wide_editors(bool p_enable) { wide_editors = p_enable; } -void EditorInspector::set_sub_inspector(bool p_enable) { - sub_inspector = p_enable; - if (!is_inside_tree()) { - return; - } +void EditorInspector::set_root_inspector(EditorInspector *p_root_inspector) { + root_inspector = p_root_inspector; + // Only the root inspector should follow focus. + set_follow_focus(false); } void EditorInspector::set_use_deletable_properties(bool p_enabled) { @@ -4099,13 +4101,13 @@ void EditorInspector::_notification(int p_what) { EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &EditorInspector::_feature_profile_changed)); set_process(is_visible_in_tree()); add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree"))); - if (!sub_inspector) { + if (!is_sub_inspector()) { get_tree()->connect("node_removed", callable_mp(this, &EditorInspector::_node_removed)); } } break; case NOTIFICATION_PREDELETE: { - if (!sub_inspector && is_inside_tree()) { + if (!is_sub_inspector() && is_inside_tree()) { get_tree()->disconnect("node_removed", callable_mp(this, &EditorInspector::_node_removed)); } edit(nullptr); @@ -4164,7 +4166,7 @@ void EditorInspector::_notification(int p_what) { case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { bool needs_update = false; - if (EditorThemeManager::is_generated_theme_outdated() && !sub_inspector) { + if (!is_sub_inspector() && EditorThemeManager::is_generated_theme_outdated()) { add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree"))); } diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h index 29ee2348832..7d64d217dcc 100644 --- a/editor/editor_inspector.h +++ b/editor/editor_inspector.h @@ -486,6 +486,7 @@ class EditorInspector : public ScrollContainer { static Ref inspector_plugins[MAX_PLUGINS]; static int inspector_plugin_count; + EditorInspector *root_inspector = nullptr; VBoxContainer *main_vbox = nullptr; // Map used to cache the instantiated editors. @@ -514,7 +515,6 @@ class EditorInspector : public ScrollContainer { bool update_all_pending = false; bool read_only = false; bool keying = false; - bool sub_inspector = false; bool wide_editors = false; bool deletable_properties = false; @@ -645,8 +645,9 @@ class EditorInspector : public ScrollContainer { String get_object_class() const; void set_use_wide_editors(bool p_enable); - void set_sub_inspector(bool p_enable); - bool is_sub_inspector() const { return sub_inspector; } + void set_root_inspector(EditorInspector *p_root_inspector); + EditorInspector *get_root_inspector() { return is_sub_inspector() ? root_inspector : this; } + bool is_sub_inspector() const { return root_inspector != nullptr; } void set_use_deletable_properties(bool p_enabled); diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index 19a4165041d..b6026be0717 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -3238,7 +3238,10 @@ void EditorPropertyResource::update_property() { sub_inspector->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED); sub_inspector->set_use_doc_hints(true); - sub_inspector->set_sub_inspector(true); + EditorInspector *parent_inspector = get_parent_inspector(); + ERR_FAIL_NULL(parent_inspector); + sub_inspector->set_root_inspector(parent_inspector->get_root_inspector()); + sub_inspector->set_property_name_style(InspectorDock::get_singleton()->get_property_name_style()); sub_inspector->connect("property_keyed", callable_mp(this, &EditorPropertyResource::_sub_inspector_property_keyed)); diff --git a/editor/inspector_dock.cpp b/editor/inspector_dock.cpp index dc07403213c..e175ad9cbcd 100644 --- a/editor/inspector_dock.cpp +++ b/editor/inspector_dock.cpp @@ -799,7 +799,7 @@ InspectorDock::InspectorDock(EditorData &p_editor_data) { inspector->set_use_folding(!bool(EDITOR_GET("interface/inspector/disable_folding"))); inspector->register_text_enter(search); - inspector->set_use_filter(true); // TODO: check me + inspector->set_use_filter(true); inspector->connect("resource_selected", callable_mp(this, &InspectorDock::_resource_selected)); From 5848d72915a60ecda673b4b192ff92680ed0c2fc Mon Sep 17 00:00:00 2001 From: AeioMuch <75151379+AeioMuch@users.noreply.github.com> Date: Sun, 18 Aug 2024 05:41:25 +0200 Subject: [PATCH 05/30] Prevent selecting the hovered node if the mouse is not inside the inspector at the end of the drag --- editor/scene_tree_dock.cpp | 40 ++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index add52ce5665..4eef95d37aa 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -87,6 +87,8 @@ void SceneTreeDock::_inspect_hovered_node() { tree_item_inspected = item; tree_item_inspected->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor))); } + EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history(); + editor_history->add_object(node_hovered_now->get_instance_id()); InspectorDock::get_inspector_singleton()->edit(node_hovered_now); InspectorDock::get_inspector_singleton()->propagate_notification(NOTIFICATION_DRAG_BEGIN); // Enable inspector drag preview after it updated. InspectorDock::get_singleton()->update(node_hovered_now); @@ -132,14 +134,6 @@ void SceneTreeDock::input(const Ref &p_event) { _push_item(pending_click_select); pending_click_select = nullptr; } - - if (mb->is_released()) { - if (tree_item_inspected) { - tree_item_inspected->clear_custom_color(0); - tree_item_inspected = nullptr; - } - _reset_hovering_timer(); - } } if (tree_clicked && get_viewport()->gui_is_dragging()) { @@ -1690,13 +1684,30 @@ void SceneTreeDock::_notification(int p_what) { case NOTIFICATION_DRAG_END: { _reset_hovering_timer(); - if (select_node_hovered_at_end_of_drag && !hovered_but_reparenting) { - Node *node_inspected = Object::cast_to(InspectorDock::get_inspector_singleton()->get_edited_object()); - if (node_inspected) { + if (tree_item_inspected) { + tree_item_inspected->clear_custom_color(0); + tree_item_inspected = nullptr; + } else { + return; + } + if (!hovered_but_reparenting) { + InspectorDock *inspector_dock = InspectorDock::get_singleton(); + if (!inspector_dock->get_rect().has_point(inspector_dock->get_local_mouse_position())) { + List full_selection = editor_selection->get_full_selected_node_list(); editor_selection->clear(); - editor_selection->add_node(node_inspected); - scene_tree->set_selected(node_inspected); - select_node_hovered_at_end_of_drag = false; + for (Node *E : full_selection) { + editor_selection->add_node(E); + } + return; + } + if (select_node_hovered_at_end_of_drag) { + Node *node_inspected = Object::cast_to(InspectorDock::get_inspector_singleton()->get_edited_object()); + if (node_inspected) { + editor_selection->clear(); + editor_selection->add_node(node_inspected); + scene_tree->set_selected(node_inspected); + select_node_hovered_at_end_of_drag = false; + } } } hovered_but_reparenting = false; @@ -4574,7 +4585,6 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec scene_tree->connect("files_dropped", callable_mp(this, &SceneTreeDock::_files_dropped)); scene_tree->connect("script_dropped", callable_mp(this, &SceneTreeDock::_script_dropped)); scene_tree->connect("nodes_dragged", callable_mp(this, &SceneTreeDock::_nodes_drag_begin)); - scene_tree->connect(SceneStringName(mouse_exited), callable_mp(this, &SceneTreeDock::_reset_hovering_timer)); scene_tree->get_scene_tree()->connect(SceneStringName(gui_input), callable_mp(this, &SceneTreeDock::_scene_tree_gui_input)); scene_tree->get_scene_tree()->connect("item_icon_double_clicked", callable_mp(this, &SceneTreeDock::_focus_node)); From 8a485ff658a9ad03dfc6bc07d0f350a97a706719 Mon Sep 17 00:00:00 2001 From: Hugo Locurcio Date: Sat, 22 Jun 2024 03:10:30 +0200 Subject: [PATCH 06/30] Fix BaseMaterial3D refracting objects located in front of the material Depth comparison is now used to prevent refraction from occurring if the pixel being refracted is located in front of the object. For pixels slightly behind the object, a `smoothstep()` curve is used to progressively increases refraction intensity as the distance between the object and the refraction increases. This avoids sudden discontinuities in the refraction. Co-authored-by: GeneralLegendary --- scene/resources/material.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index 927e76e4b2d..4ee3802b080 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -946,7 +946,7 @@ uniform vec4 refraction_texture_channel; code += "uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_linear_mipmap;\n"; } - if (proximity_fade_enabled) { + if (features[FEATURE_REFRACTION] || proximity_fade_enabled) { code += "uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;\n"; } @@ -1627,7 +1627,14 @@ void fragment() {)"; } code += R"( float ref_amount = 1.0 - albedo.a * albedo_tex.a; - EMISSION += textureLod(screen_texture, ref_ofs, ROUGHNESS * 8.0).rgb * ref_amount * EXPOSURE; + + float refraction_depth_tex = textureLod(depth_texture, ref_ofs, 0.0).r; + vec4 refraction_view_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, refraction_depth_tex, 1.0); + refraction_view_pos.xyz /= refraction_view_pos.w; + + // If the depth buffer is lower then the model's Z position, use the refracted UV, otherwise use the normal screen UV. + // At low depth differences, decrease refraction intensity to avoid sudden discontinuities. + EMISSION += textureLod(screen_texture, mix(SCREEN_UV, ref_ofs, smoothstep(0.0, 1.0, VERTEX.z - refraction_view_pos.z)), ROUGHNESS * 8.0).rgb * ref_amount * EXPOSURE; ALBEDO *= 1.0 - ref_amount; // Force transparency on the material (required for refraction). ALPHA = 1.0; @@ -1649,10 +1656,10 @@ void fragment() {)"; if (proximity_fade_enabled) { code += R"( // Proximity Fade: Enabled - float depth_tex = textureLod(depth_texture, SCREEN_UV, 0.0).r; - vec4 world_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, depth_tex, 1.0); - world_pos.xyz /= world_pos.w; - ALPHA *= clamp(1.0 - smoothstep(world_pos.z + proximity_fade_distance, world_pos.z, VERTEX.z), 0.0, 1.0); + float proximity_depth_tex = textureLod(depth_texture, SCREEN_UV, 0.0).r; + vec4 proximity_view_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, proximity_depth_tex, 1.0); + proximity_view_pos.xyz /= proximity_view_pos.w; + ALPHA *= clamp(1.0 - smoothstep(proximity_view_pos.z + proximity_fade_distance, proximity_view_pos.z, VERTEX.z), 0.0, 1.0); )"; } From 6516ca6b11a6241d7908eb322343d44c10050d98 Mon Sep 17 00:00:00 2001 From: Haoyu Qiu Date: Wed, 22 May 2024 10:22:50 +0800 Subject: [PATCH 07/30] Parse fragment from URL --- core/string/ustring.cpp | 25 ++++++++++-- core/string/ustring.h | 2 +- editor/debugger/editor_debugger_server.cpp | 4 +- .../plugins/asset_library_editor_plugin.cpp | 3 +- .../editor_debugger_server_websocket.cpp | 4 +- modules/websocket/emws_peer.cpp | 3 +- modules/websocket/wsl_peer.cpp | 3 +- scene/main/http_request.cpp | 3 +- tests/core/string/test_string.h | 40 +++++++++++++++++++ 9 files changed, 74 insertions(+), 13 deletions(-) diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index 391a203d5b5..e6f7492a189 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -221,18 +221,35 @@ void CharString::copy_from(const char *p_cstr) { /* String */ /*************************************************************************/ -Error String::parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path) const { - // Splits the URL into scheme, host, port, path. Strip credentials when present. +Error String::parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path, String &r_fragment) const { + // Splits the URL into scheme, host, port, path, fragment. Strip credentials when present. String base = *this; r_scheme = ""; r_host = ""; r_port = 0; r_path = ""; + r_fragment = ""; + int pos = base.find("://"); // Scheme if (pos != -1) { - r_scheme = base.substr(0, pos + 3).to_lower(); - base = base.substr(pos + 3, base.length() - pos - 3); + bool is_scheme_valid = true; + for (int i = 0; i < pos; i++) { + if (!is_ascii_alphanumeric_char(base[i]) && base[i] != '+' && base[i] != '-' && base[i] != '.') { + is_scheme_valid = false; + break; + } + } + if (is_scheme_valid) { + r_scheme = base.substr(0, pos + 3).to_lower(); + base = base.substr(pos + 3, base.length() - pos - 3); + } + } + pos = base.find("#"); + // Fragment + if (pos != -1) { + r_fragment = base.substr(pos + 1); + base = base.substr(0, pos); } pos = base.find("/"); // Path diff --git a/core/string/ustring.h b/core/string/ustring.h index 5d4b209c252..aa62c9cb188 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -452,7 +452,7 @@ class String { String c_escape_multiline() const; String c_unescape() const; String json_escape() const; - Error parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path) const; + Error parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path, String &r_fragment) const; String property_name_encode() const; diff --git a/editor/debugger/editor_debugger_server.cpp b/editor/debugger/editor_debugger_server.cpp index c0efc6a1fc6..9ec60581324 100644 --- a/editor/debugger/editor_debugger_server.cpp +++ b/editor/debugger/editor_debugger_server.cpp @@ -77,8 +77,8 @@ Error EditorDebuggerServerTCP::start(const String &p_uri) { // Optionally override if (!p_uri.is_empty() && p_uri != "tcp://") { - String scheme, path; - Error err = p_uri.parse_url(scheme, bind_host, bind_port, path); + String scheme, path, fragment; + Error err = p_uri.parse_url(scheme, bind_host, bind_port, path, fragment); ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER); } diff --git a/editor/plugins/asset_library_editor_plugin.cpp b/editor/plugins/asset_library_editor_plugin.cpp index af4b3a1643b..fec8d4b8972 100644 --- a/editor/plugins/asset_library_editor_plugin.cpp +++ b/editor/plugins/asset_library_editor_plugin.cpp @@ -993,7 +993,8 @@ void EditorAssetLibrary::_request_image(ObjectID p_for, int p_asset_id, String p String url_host; int url_port; String url_path; - Error err = trimmed_url.parse_url(url_scheme, url_host, url_port, url_path); + String url_fragment; + Error err = trimmed_url.parse_url(url_scheme, url_host, url_port, url_path, url_fragment); if (err != OK) { if (is_print_verbose_enabled()) { ERR_PRINT(vformat("Asset Library: Invalid image URL '%s' for asset # %d.", trimmed_url, p_asset_id)); diff --git a/modules/websocket/editor/editor_debugger_server_websocket.cpp b/modules/websocket/editor/editor_debugger_server_websocket.cpp index a28fc53440e..344a0356c5a 100644 --- a/modules/websocket/editor/editor_debugger_server_websocket.cpp +++ b/modules/websocket/editor/editor_debugger_server_websocket.cpp @@ -77,8 +77,8 @@ Error EditorDebuggerServerWebSocket::start(const String &p_uri) { // Optionally override if (!p_uri.is_empty() && p_uri != "ws://") { - String scheme, path; - Error err = p_uri.parse_url(scheme, bind_host, bind_port, path); + String scheme, path, fragment; + Error err = p_uri.parse_url(scheme, bind_host, bind_port, path, fragment); ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER); } diff --git a/modules/websocket/emws_peer.cpp b/modules/websocket/emws_peer.cpp index 03a530909bf..c5768c9f0b3 100644 --- a/modules/websocket/emws_peer.cpp +++ b/modules/websocket/emws_peer.cpp @@ -68,8 +68,9 @@ Error EMWSPeer::connect_to_url(const String &p_url, Ref p_tls_option String host; String path; String scheme; + String fragment; int port = 0; - Error err = p_url.parse_url(scheme, host, port, path); + Error err = p_url.parse_url(scheme, host, port, path, fragment); ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); if (scheme.is_empty()) { diff --git a/modules/websocket/wsl_peer.cpp b/modules/websocket/wsl_peer.cpp index 0a9a4053e3a..0c0a046805c 100644 --- a/modules/websocket/wsl_peer.cpp +++ b/modules/websocket/wsl_peer.cpp @@ -482,8 +482,9 @@ Error WSLPeer::connect_to_url(const String &p_url, Ref p_options) { String host; String path; String scheme; + String fragment; int port = 0; - Error err = p_url.parse_url(scheme, host, port, path); + Error err = p_url.parse_url(scheme, host, port, path, fragment); ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); if (scheme.is_empty()) { scheme = "ws://"; diff --git a/scene/main/http_request.cpp b/scene/main/http_request.cpp index 3469b806a6e..85266110935 100644 --- a/scene/main/http_request.cpp +++ b/scene/main/http_request.cpp @@ -49,7 +49,8 @@ Error HTTPRequest::_parse_url(const String &p_url) { redirections = 0; String scheme; - Error err = p_url.parse_url(scheme, url, port, request_string); + String fragment; + Error err = p_url.parse_url(scheme, url, port, request_string, fragment); ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Error parsing URL: '%s'.", p_url)); if (scheme == "https://") { diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h index 301771a3de1..0131f4c02a6 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -1988,6 +1988,46 @@ TEST_CASE("[String] Variant ptr indexed set") { CHECK_EQ(s, String("azcd")); } +TEST_CASE("[String] parse_url") { + String scheme, host, path, fragment; + int port; + + SUBCASE("Typical URL") { + Error err = String("https://docs.godotengine.org/en/stable/").parse_url(scheme, host, port, path, fragment); + REQUIRE(err == OK); + CHECK_EQ(scheme, "https://"); + CHECK_EQ(host, "docs.godotengine.org"); + CHECK_EQ(port, 0); + CHECK_EQ(path, "/en/stable/"); + CHECK_EQ(fragment, ""); + } + + SUBCASE("All Elements") { + Error err = String("https://www.example.com:8080/path/to/file.html#fragment").parse_url(scheme, host, port, path, fragment); + REQUIRE(err == OK); + CHECK_EQ(scheme, "https://"); + CHECK_EQ(host, "www.example.com"); + CHECK_EQ(port, 8080); + CHECK_EQ(path, "/path/to/file.html"); + CHECK_EQ(fragment, "fragment"); + } + + SUBCASE("Invalid Scheme") { + Error err = String("http_://example.com").parse_url(scheme, host, port, path, fragment); + REQUIRE(err == ERR_INVALID_PARAMETER); // Host being empty is an error. + } + + SUBCASE("Scheme vs Fragment") { + Error err = String("google.com/#goto=http://redirect_url/").parse_url(scheme, host, port, path, fragment); + REQUIRE(err == OK); + CHECK_EQ(scheme, ""); + CHECK_EQ(host, "google.com"); + CHECK_EQ(port, 0); + CHECK_EQ(path, "/"); + CHECK_EQ(fragment, "goto=http://redirect_url/"); + } +} + TEST_CASE("[Stress][String] Empty via ' == String()'") { for (int i = 0; i < 100000; ++i) { String str = "Hello World!"; From 71ac6b7476ccdf97ba99f44a64ed6212c31ab042 Mon Sep 17 00:00:00 2001 From: MewPurPur Date: Fri, 12 Jul 2024 01:52:10 +0300 Subject: [PATCH 08/30] Optimize StyleBoxFlat.draw() --- scene/resources/style_box_flat.cpp | 147 ++++++++++++++--------------- 1 file changed, 70 insertions(+), 77 deletions(-) diff --git a/scene/resources/style_box_flat.cpp b/scene/resources/style_box_flat.cpp index 60b91ef0cb3..15816925c1f 100644 --- a/scene/resources/style_box_flat.cpp +++ b/scene/resources/style_box_flat.cpp @@ -226,33 +226,16 @@ inline void set_inner_corner_radius(const Rect2 style_rect, const Rect2 inner_re real_t border_right = style_rect.size.width - inner_rect.size.width - border_left; real_t border_bottom = style_rect.size.height - inner_rect.size.height - border_top; - real_t rad; - - // Top left. - rad = MIN(border_top, border_left); - inner_corner_radius[0] = MAX(corner_radius[0] - rad, 0); - - // Top right; - rad = MIN(border_top, border_right); - inner_corner_radius[1] = MAX(corner_radius[1] - rad, 0); - - // Bottom right. - rad = MIN(border_bottom, border_right); - inner_corner_radius[2] = MAX(corner_radius[2] - rad, 0); - - // Bottom left. - rad = MIN(border_bottom, border_left); - inner_corner_radius[3] = MAX(corner_radius[3] - rad, 0); + inner_corner_radius[0] = MAX(corner_radius[0] - MIN(border_top, border_left), 0); // Top left. + inner_corner_radius[1] = MAX(corner_radius[1] - MIN(border_top, border_right), 0); // Top right. + inner_corner_radius[2] = MAX(corner_radius[2] - MIN(border_bottom, border_right), 0); // Bottom right. + inner_corner_radius[3] = MAX(corner_radius[3] - MIN(border_bottom, border_left), 0); // Bottom left. } inline void draw_rounded_rectangle(Vector &verts, Vector &indices, Vector &colors, const Rect2 &style_rect, const real_t corner_radius[4], const Rect2 &ring_rect, const Rect2 &inner_rect, const Color &inner_color, const Color &outer_color, const int corner_detail, const Vector2 &skew, bool is_filled = false) { int vert_offset = verts.size(); - if (!vert_offset) { - vert_offset = 0; - } - - int adapted_corner_detail = (corner_radius[0] == 0 && corner_radius[1] == 0 && corner_radius[2] == 0 && corner_radius[3] == 0) ? 1 : corner_detail; + int adapted_corner_detail = (corner_radius[0] > 0) || (corner_radius[1] > 0) || (corner_radius[2] > 0) || (corner_radius[3] > 0) ? corner_detail : 1; bool draw_border = !is_filled; @@ -280,30 +263,44 @@ inline void draw_rounded_rectangle(Vector &verts, Vector &indices, // If the center is filled, we do not draw the border and directly use the inner ring as reference. Because all calls to this // method either draw a ring or a filled rounded rectangle, but not both. - int max_inner_outer = draw_border ? 2 : 1; - - for (int corner_index = 0; corner_index < 4; corner_index++) { + real_t quarter_arc_rad = Math_PI / 2.0; + Point2 style_rect_center = style_rect.get_center(); + + int colors_size = colors.size(); + int verts_size = verts.size(); + int new_verts_amount = (adapted_corner_detail + 1) * (draw_border ? 8 : 4); + colors.resize(colors_size + new_verts_amount); + verts.resize(verts_size + new_verts_amount); + Color *colors_ptr = colors.ptrw(); + Vector2 *verts_ptr = verts.ptrw(); + + for (int corner_idx = 0; corner_idx < 4; corner_idx++) { for (int detail = 0; detail <= adapted_corner_detail; detail++) { - for (int inner_outer = 0; inner_outer < max_inner_outer; inner_outer++) { - real_t radius; - Color color; - Point2 corner_point; - if (inner_outer == 0) { - radius = inner_corner_radius[corner_index]; - color = inner_color; - corner_point = inner_points[corner_index]; - } else { - radius = ring_corner_radius[corner_index]; - color = outer_color; - corner_point = outer_points[corner_index]; - } + int idx_ofs = (adapted_corner_detail + 1) * corner_idx + detail; + if (draw_border) { + idx_ofs *= 2; + } - const real_t x = radius * (real_t)cos((corner_index + detail / (double)adapted_corner_detail) * (Math_TAU / 4.0) + Math_PI) + corner_point.x; - const real_t y = radius * (real_t)sin((corner_index + detail / (double)adapted_corner_detail) * (Math_TAU / 4.0) + Math_PI) + corner_point.y; - const float x_skew = -skew.x * (y - style_rect.get_center().y); - const float y_skew = -skew.y * (x - style_rect.get_center().x); - verts.push_back(Vector2(x + x_skew, y + y_skew)); - colors.push_back(color); + const real_t pt_angle = (corner_idx + detail / (double)adapted_corner_detail) * quarter_arc_rad + Math_PI; + const real_t angle_cosine = cos(pt_angle); + const real_t angle_sine = sin(pt_angle); + + { + const real_t x = inner_corner_radius[corner_idx] * angle_cosine + inner_points[corner_idx].x; + const real_t y = inner_corner_radius[corner_idx] * angle_sine + inner_points[corner_idx].y; + const float x_skew = -skew.x * (y - style_rect_center.y); + const float y_skew = -skew.y * (x - style_rect_center.x); + verts_ptr[verts_size + idx_ofs] = Vector2(x + x_skew, y + y_skew); + colors_ptr[colors_size + idx_ofs] = inner_color; + } + + if (draw_border) { + const real_t x = ring_corner_radius[corner_idx] * angle_cosine + outer_points[corner_idx].x; + const real_t y = ring_corner_radius[corner_idx] * angle_sine + outer_points[corner_idx].y; + const float x_skew = -skew.x * (y - style_rect_center.y); + const float y_skew = -skew.y * (x - style_rect_center.x); + verts_ptr[verts_size + idx_ofs + 1] = Vector2(x + x_skew, y + y_skew); + colors_ptr[colors_size + idx_ofs + 1] = outer_color; } } } @@ -313,10 +310,15 @@ inline void draw_rounded_rectangle(Vector &verts, Vector &indices, // Fill the indices and the colors for the border. if (draw_border) { + int indices_size = indices.size(); + indices.resize(indices_size + ring_vert_count * 3); + int *indices_ptr = indices.ptrw(); + for (int i = 0; i < ring_vert_count; i++) { - indices.push_back(vert_offset + ((i + 0) % ring_vert_count)); - indices.push_back(vert_offset + ((i + 2) % ring_vert_count)); - indices.push_back(vert_offset + ((i + 1) % ring_vert_count)); + int idx_ofs = indices_size + i * 3; + indices_ptr[idx_ofs] = vert_offset + i % ring_vert_count; + indices_ptr[idx_ofs + 1] = vert_offset + (i + 2) % ring_vert_count; + indices_ptr[idx_ofs + 2] = vert_offset + (i + 1) % ring_vert_count; } } @@ -327,40 +329,30 @@ inline void draw_rounded_rectangle(Vector &verts, Vector &indices, int stripes_count = ring_vert_count / 2 - 1; int last_vert_id = ring_vert_count - 1; + int indices_size = indices.size(); + indices.resize(indices_size + stripes_count * 6); + int *indices_ptr = indices.ptrw(); + for (int i = 0; i < stripes_count; i++) { + int idx_ofs = indices_size + i * 6; // Polygon 1. - indices.push_back(vert_offset + i); - indices.push_back(vert_offset + last_vert_id - i - 1); - indices.push_back(vert_offset + i + 1); + indices_ptr[idx_ofs] = vert_offset + i; + indices_ptr[idx_ofs + 1] = vert_offset + last_vert_id - i - 1; + indices_ptr[idx_ofs + 2] = vert_offset + i + 1; // Polygon 2. - indices.push_back(vert_offset + i); - indices.push_back(vert_offset + last_vert_id - 0 - i); - indices.push_back(vert_offset + last_vert_id - 1 - i); + indices_ptr[idx_ofs + 3] = vert_offset + i; + indices_ptr[idx_ofs + 4] = vert_offset + last_vert_id - i; + indices_ptr[idx_ofs + 5] = vert_offset + last_vert_id - i - 1; } } } inline void adapt_values(int p_index_a, int p_index_b, real_t *adapted_values, const real_t *p_values, const real_t p_width, const real_t p_max_a, const real_t p_max_b) { - if (p_values[p_index_a] + p_values[p_index_b] > p_width) { - real_t factor; - real_t new_value; - - factor = (real_t)p_width / (real_t)(p_values[p_index_a] + p_values[p_index_b]); - - new_value = (p_values[p_index_a] * factor); - if (new_value < adapted_values[p_index_a]) { - adapted_values[p_index_a] = new_value; - } - new_value = (p_values[p_index_b] * factor); - if (new_value < adapted_values[p_index_b]) { - adapted_values[p_index_b] = new_value; - } - } else { - adapted_values[p_index_a] = MIN(p_values[p_index_a], adapted_values[p_index_a]); - adapted_values[p_index_b] = MIN(p_values[p_index_b], adapted_values[p_index_b]); - } - adapted_values[p_index_a] = MIN(p_max_a, adapted_values[p_index_a]); - adapted_values[p_index_b] = MIN(p_max_b, adapted_values[p_index_b]); + real_t value_a = p_values[p_index_a]; + real_t value_b = p_values[p_index_b]; + real_t factor = MIN(1.0, p_width / (value_a + value_b)); + adapted_values[p_index_a] = MIN(MIN(value_a * factor, p_max_a), adapted_values[p_index_a]); + adapted_values[p_index_b] = MIN(MIN(value_b * factor, p_max_b), adapted_values[p_index_b]); } Rect2 StyleBoxFlat::get_draw_rect(const Rect2 &p_rect) const { @@ -388,7 +380,7 @@ void StyleBoxFlat::draw(RID p_canvas_item, const Rect2 &p_rect) const { } const bool rounded_corners = (corner_radius[0] > 0) || (corner_radius[1] > 0) || (corner_radius[2] > 0) || (corner_radius[3] > 0); - // Only enable antialiasing if it is actually needed. This improve performances + // Only enable antialiasing if it is actually needed. This improves performance // and maximizes sharpness for non-skewed StyleBoxes with sharp corners. const bool aa_on = (rounded_corners || !skew.is_zero_approx()) && anti_aliased; @@ -428,7 +420,7 @@ void StyleBoxFlat::draw(RID p_canvas_item, const Rect2 &p_rect) const { Vector colors; Vector uvs; - // Create shadow + // Create shadow. if (draw_shadow) { Rect2 shadow_inner_rect = style_rect; shadow_inner_rect.position += shadow_offset; @@ -538,9 +530,10 @@ void StyleBoxFlat::draw(RID p_canvas_item, const Rect2 &p_rect) const { // Compute UV coordinates. Rect2 uv_rect = style_rect.grow(aa_on ? aa_size : 0); uvs.resize(verts.size()); + Point2 *uvs_ptr = uvs.ptrw(); for (int i = 0; i < verts.size(); i++) { - uvs.write[i].x = (verts[i].x - uv_rect.position.x) / uv_rect.size.width; - uvs.write[i].y = (verts[i].y - uv_rect.position.y) / uv_rect.size.height; + uvs_ptr[i].x = (verts[i].x - uv_rect.position.x) / uv_rect.size.width; + uvs_ptr[i].y = (verts[i].y - uv_rect.position.y) / uv_rect.size.height; } // Draw stylebox. From 38579a1e847132428019dbc73d88c91eb4f08586 Mon Sep 17 00:00:00 2001 From: Haoyu Qiu Date: Fri, 27 Sep 2024 15:51:54 +0800 Subject: [PATCH 09/30] Unify editor version buttons --- editor/editor_about.cpp | 30 +--------- editor/editor_about.h | 5 -- editor/gui/editor_bottom_panel.cpp | 29 +--------- editor/gui/editor_bottom_panel.h | 3 - editor/gui/editor_version_button.cpp | 85 ++++++++++++++++++++++++++++ editor/gui/editor_version_button.h | 61 ++++++++++++++++++++ editor/project_manager.cpp | 24 +------- editor/project_manager.h | 7 --- 8 files changed, 152 insertions(+), 92 deletions(-) create mode 100644 editor/gui/editor_version_button.cpp create mode 100644 editor/gui/editor_version_button.h diff --git a/editor/editor_about.cpp b/editor/editor_about.cpp index dc943fc783c..34f432aa7e7 100644 --- a/editor/editor_about.cpp +++ b/editor/editor_about.cpp @@ -33,16 +33,12 @@ #include "core/authors.gen.h" #include "core/donors.gen.h" #include "core/license.gen.h" -#include "core/os/time.h" -#include "core/version.h" #include "editor/editor_string_names.h" +#include "editor/gui/editor_version_button.h" #include "editor/themes/editor_scale.h" #include "scene/gui/item_list.h" #include "scene/resources/style_box.h" -// The metadata key used to store and retrieve the version text to copy to the clipboard. -const String EditorAbout::META_TEXT_TO_COPY = "text_to_copy"; - void EditorAbout::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { @@ -81,10 +77,6 @@ void EditorAbout::_license_tree_selected() { _tpl_text->set_text(selected->get_metadata(0)); } -void EditorAbout::_version_button_pressed() { - DisplayServer::get_singleton()->clipboard_set(version_btn->get_meta(META_TEXT_TO_COPY)); -} - void EditorAbout::_item_with_website_selected(int p_id, ItemList *p_il) { const String website = p_il->get_item_metadata(p_id); if (!website.is_empty()) { @@ -198,25 +190,7 @@ EditorAbout::EditorAbout() { Control *v_spacer = memnew(Control); version_info_vbc->add_child(v_spacer); - version_btn = memnew(LinkButton); - String hash = String(VERSION_HASH); - if (hash.length() != 0) { - hash = " " + vformat("[%s]", hash.left(9)); - } - version_btn->set_text(VERSION_FULL_NAME + hash); - // Set the text to copy in metadata as it slightly differs from the button's text. - version_btn->set_meta(META_TEXT_TO_COPY, "v" VERSION_FULL_BUILD + hash); - version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER); - String build_date; - if (VERSION_TIMESTAMP > 0) { - build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC"; - } else { - build_date = TTR("(unknown)"); - } - version_btn->set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version number."), build_date)); - - version_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorAbout::_version_button_pressed)); - version_info_vbc->add_child(version_btn); + version_info_vbc->add_child(memnew(EditorVersionButton(EditorVersionButton::FORMAT_WITH_NAME_AND_BUILD))); Label *about_text = memnew(Label); about_text->set_v_size_flags(Control::SIZE_SHRINK_CENTER); diff --git a/editor/editor_about.h b/editor/editor_about.h index fc3d6cedce5..6f33d502d7e 100644 --- a/editor/editor_about.h +++ b/editor/editor_about.h @@ -33,7 +33,6 @@ #include "scene/gui/dialogs.h" #include "scene/gui/item_list.h" -#include "scene/gui/link_button.h" #include "scene/gui/rich_text_label.h" #include "scene/gui/scroll_container.h" #include "scene/gui/separator.h" @@ -49,16 +48,12 @@ class EditorAbout : public AcceptDialog { GDCLASS(EditorAbout, AcceptDialog); - static const String META_TEXT_TO_COPY; - private: void _license_tree_selected(); - void _version_button_pressed(); void _item_with_website_selected(int p_id, ItemList *p_il); void _item_list_resized(ItemList *p_il); ScrollContainer *_populate_list(const String &p_name, const List &p_sections, const char *const *const p_src[], int p_single_column_flags = 0, bool p_allow_website = false); - LinkButton *version_btn = nullptr; Tree *_tpl_tree = nullptr; RichTextLabel *license_text_label = nullptr; RichTextLabel *_tpl_text = nullptr; diff --git a/editor/gui/editor_bottom_panel.cpp b/editor/gui/editor_bottom_panel.cpp index 4b2fd9cb2f1..f6ba74fe955 100644 --- a/editor/gui/editor_bottom_panel.cpp +++ b/editor/gui/editor_bottom_panel.cpp @@ -30,8 +30,6 @@ #include "editor_bottom_panel.h" -#include "core/os/time.h" -#include "core/version.h" #include "editor/debugger/editor_debugger_node.h" #include "editor/editor_about.h" #include "editor/editor_command_palette.h" @@ -39,13 +37,10 @@ #include "editor/editor_string_names.h" #include "editor/engine_update_label.h" #include "editor/gui/editor_toaster.h" +#include "editor/gui/editor_version_button.h" #include "editor/themes/editor_scale.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" -#include "scene/gui/link_button.h" - -// The metadata key used to store and retrieve the version text to copy to the clipboard. -static const String META_TEXT_TO_COPY = "text_to_copy"; void EditorBottomPanel::_notification(int p_what) { switch (p_what) { @@ -110,10 +105,6 @@ void EditorBottomPanel::_expand_button_toggled(bool p_pressed) { EditorNode::get_top_split()->set_visible(!p_pressed); } -void EditorBottomPanel::_version_button_pressed() { - DisplayServer::get_singleton()->clipboard_set(version_btn->get_meta(META_TEXT_TO_COPY)); -} - bool EditorBottomPanel::_button_drag_hover(const Vector2 &, const Variant &, Button *p_button, Control *p_control) { if (!p_button->is_pressed()) { _switch_by_control(true, p_control); @@ -262,25 +253,9 @@ EditorBottomPanel::EditorBottomPanel() { editor_toaster = memnew(EditorToaster); bottom_hbox->add_child(editor_toaster); - version_btn = memnew(LinkButton); - version_btn->set_text(VERSION_FULL_CONFIG); - String hash = String(VERSION_HASH); - if (hash.length() != 0) { - hash = " " + vformat("[%s]", hash.left(9)); - } - // Set the text to copy in metadata as it slightly differs from the button's text. - version_btn->set_meta(META_TEXT_TO_COPY, "v" VERSION_FULL_BUILD + hash); + EditorVersionButton *version_btn = memnew(EditorVersionButton(EditorVersionButton::FORMAT_BASIC)); // Fade out the version label to be less prominent, but still readable. version_btn->set_self_modulate(Color(1, 1, 1, 0.65)); - version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER); - String build_date; - if (VERSION_TIMESTAMP > 0) { - build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC"; - } else { - build_date = TTR("(unknown)"); - } - version_btn->set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version information."), build_date)); - version_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorBottomPanel::_version_button_pressed)); version_btn->set_v_size_flags(Control::SIZE_SHRINK_CENTER); bottom_hbox->add_child(version_btn); diff --git a/editor/gui/editor_bottom_panel.h b/editor/gui/editor_bottom_panel.h index 95c767dae5d..3d44b3750af 100644 --- a/editor/gui/editor_bottom_panel.h +++ b/editor/gui/editor_bottom_panel.h @@ -37,7 +37,6 @@ class Button; class ConfigFile; class EditorToaster; class HBoxContainer; -class LinkButton; class VBoxContainer; class EditorBottomPanel : public PanelContainer { @@ -55,14 +54,12 @@ class EditorBottomPanel : public PanelContainer { HBoxContainer *bottom_hbox = nullptr; HBoxContainer *button_hbox = nullptr; EditorToaster *editor_toaster = nullptr; - LinkButton *version_btn = nullptr; Button *expand_button = nullptr; Control *last_opened_control = nullptr; void _switch_by_control(bool p_visible, Control *p_control); void _switch_to_item(bool p_visible, int p_idx); void _expand_button_toggled(bool p_pressed); - void _version_button_pressed(); bool _button_drag_hover(const Vector2 &, const Variant &, Button *p_button, Control *p_control); diff --git a/editor/gui/editor_version_button.cpp b/editor/gui/editor_version_button.cpp new file mode 100644 index 00000000000..635d66f42a9 --- /dev/null +++ b/editor/gui/editor_version_button.cpp @@ -0,0 +1,85 @@ +/**************************************************************************/ +/* editor_version_button.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_version_button.h" + +#include "core/os/time.h" +#include "core/version.h" + +String _get_version_string(EditorVersionButton::VersionFormat p_format) { + String main; + switch (p_format) { + case EditorVersionButton::FORMAT_BASIC: { + return VERSION_FULL_CONFIG; + } break; + case EditorVersionButton::FORMAT_WITH_BUILD: { + main = "v" VERSION_FULL_BUILD; + } break; + case EditorVersionButton::FORMAT_WITH_NAME_AND_BUILD: { + main = VERSION_FULL_NAME; + } break; + default: { + ERR_FAIL_V_MSG(VERSION_FULL_NAME, "Unexpected format: " + itos(p_format)); + } break; + } + + String hash = VERSION_HASH; + if (!hash.is_empty()) { + hash = vformat(" [%s]", hash.left(9)); + } + return main + hash; +} + +void EditorVersionButton::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_POSTINITIALIZE: { + // This can't be done in the constructor because theme cache is not ready yet. + set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + set_text(_get_version_string(format)); + } break; + } +} + +void EditorVersionButton::pressed() { + DisplayServer::get_singleton()->clipboard_set(_get_version_string(FORMAT_WITH_BUILD)); +} + +EditorVersionButton::EditorVersionButton(VersionFormat p_format) { + format = p_format; + set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER); + + String build_date; + if (VERSION_TIMESTAMP > 0) { + build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC"; + } else { + build_date = TTR("(unknown)"); + } + set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version information."), build_date)); +} diff --git a/editor/gui/editor_version_button.h b/editor/gui/editor_version_button.h new file mode 100644 index 00000000000..591c3d483ee --- /dev/null +++ b/editor/gui/editor_version_button.h @@ -0,0 +1,61 @@ +/**************************************************************************/ +/* editor_version_button.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef EDITOR_VERSION_BUTTON_H +#define EDITOR_VERSION_BUTTON_H + +#include "scene/gui/link_button.h" + +class EditorVersionButton : public LinkButton { + GDCLASS(EditorVersionButton, LinkButton); + +public: + enum VersionFormat { + // 4.3.2.stable + FORMAT_BASIC, + // v4.3.2.stable.mono [HASH] + FORMAT_WITH_BUILD, + // Godot Engine v4.3.2.stable.mono.official [HASH] + FORMAT_WITH_NAME_AND_BUILD, + }; + +private: + VersionFormat format = FORMAT_WITH_NAME_AND_BUILD; + +protected: + void _notification(int p_what); + + virtual void pressed() override; + +public: + EditorVersionButton(VersionFormat p_format); +}; + +#endif // EDITOR_VERSION_BUTTON_H diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 8411c0edea1..30878a24880 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -38,7 +38,6 @@ #include "core/io/stream_peer_tls.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/os/time.h" #include "core/version.h" #include "editor/editor_about.h" #include "editor/editor_settings.h" @@ -46,6 +45,7 @@ #include "editor/engine_update_label.h" #include "editor/gui/editor_file_dialog.h" #include "editor/gui/editor_title_bar.h" +#include "editor/gui/editor_version_button.h" #include "editor/plugins/asset_library_editor_plugin.h" #include "editor/project_manager/project_dialog.h" #include "editor/project_manager/project_list.h" @@ -398,12 +398,6 @@ void ProjectManager::_restart_confirmed() { get_tree()->quit(); } -// Footer. - -void ProjectManager::_version_button_pressed() { - DisplayServer::get_singleton()->clipboard_set(version_btn->get_text()); -} - // Project list. void ProjectManager::_update_list_placeholder() { @@ -1459,23 +1453,9 @@ ProjectManager::ProjectManager() { update_label->connect("offline_clicked", callable_mp(this, &ProjectManager::_show_quick_settings)); #endif - version_btn = memnew(LinkButton); - String hash = String(VERSION_HASH); - if (hash.length() != 0) { - hash = " " + vformat("[%s]", hash.left(9)); - } - version_btn->set_text("v" VERSION_FULL_BUILD + hash); + EditorVersionButton *version_btn = memnew(EditorVersionButton(EditorVersionButton::FORMAT_WITH_BUILD)); // Fade the version label to be less prominent, but still readable. version_btn->set_self_modulate(Color(1, 1, 1, 0.6)); - version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER); - String build_date; - if (VERSION_TIMESTAMP > 0) { - build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC"; - } else { - build_date = TTR("(unknown)"); - } - version_btn->set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version information."), build_date)); - version_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_version_button_pressed)); footer_bar->add_child(version_btn); } diff --git a/editor/project_manager.h b/editor/project_manager.h index aad51d0e985..07da0059c01 100644 --- a/editor/project_manager.h +++ b/editor/project_manager.h @@ -41,7 +41,6 @@ class EditorFileDialog; class EditorTitleBar; class HFlowContainer; class LineEdit; -class LinkButton; class MarginContainer; class OptionButton; class PanelContainer; @@ -124,12 +123,6 @@ class ProjectManager : public Control { void _show_quick_settings(); void _restart_confirmed(); - // Footer. - - LinkButton *version_btn = nullptr; - - void _version_button_pressed(); - // Project list. VBoxContainer *empty_list_placeholder = nullptr; From 8a41b1d90ff447fb3014b7402f28f820ddc7c8a6 Mon Sep 17 00:00:00 2001 From: Gergely Kis Date: Sun, 14 Jul 2024 09:21:31 +0200 Subject: [PATCH 10/30] GDExtension: Use loader to check if the library exists. --- core/extension/gdextension_library_loader.cpp | 4 ++++ core/extension/gdextension_library_loader.h | 1 + core/extension/gdextension_loader.h | 1 + core/extension/gdextension_manager.cpp | 3 ++- 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/extension/gdextension_library_loader.cpp b/core/extension/gdextension_library_loader.cpp index 5ba4933c357..d5f2eb668f5 100644 --- a/core/extension/gdextension_library_loader.cpp +++ b/core/extension/gdextension_library_loader.cpp @@ -259,6 +259,10 @@ bool GDExtensionLibraryLoader::has_library_changed() const { return false; } +bool GDExtensionLibraryLoader::library_exists() const { + return FileAccess::exists(resource_path); +} + Error GDExtensionLibraryLoader::parse_gdextension_file(const String &p_path) { resource_path = p_path; diff --git a/core/extension/gdextension_library_loader.h b/core/extension/gdextension_library_loader.h index f4372a75d41..f781611b305 100644 --- a/core/extension/gdextension_library_loader.h +++ b/core/extension/gdextension_library_loader.h @@ -77,6 +77,7 @@ class GDExtensionLibraryLoader : public GDExtensionLoader { virtual void close_library() override; virtual bool is_library_open() const override; virtual bool has_library_changed() const override; + virtual bool library_exists() const override; Error parse_gdextension_file(const String &p_path); }; diff --git a/core/extension/gdextension_loader.h b/core/extension/gdextension_loader.h index 7d779858b72..22895503291 100644 --- a/core/extension/gdextension_loader.h +++ b/core/extension/gdextension_loader.h @@ -42,6 +42,7 @@ class GDExtensionLoader : public RefCounted { virtual void close_library() = 0; virtual bool is_library_open() const = 0; virtual bool has_library_changed() const = 0; + virtual bool library_exists() const = 0; }; #endif // GDEXTENSION_LOADER_H diff --git a/core/extension/gdextension_manager.cpp b/core/extension/gdextension_manager.cpp index 01efe0d96e9..fff938858fd 100644 --- a/core/extension/gdextension_manager.cpp +++ b/core/extension/gdextension_manager.cpp @@ -302,7 +302,8 @@ bool GDExtensionManager::ensure_extensions_loaded(const HashSet &p_exten for (const String &loaded_extension : loaded_extensions) { if (!p_extensions.has(loaded_extension)) { // The extension may not have a .gdextension file. - if (!FileAccess::exists(loaded_extension)) { + const Ref extension = GDExtensionManager::get_singleton()->get_extension(loaded_extension); + if (!extension->get_loader()->library_exists()) { extensions_removed.push_back(loaded_extension); } } From 529897cb0c7ebddf9786752cf4f5fe81330bb55c Mon Sep 17 00:00:00 2001 From: BlueCube3310 <53150244+BlueCube3310@users.noreply.github.com> Date: Sun, 29 Sep 2024 10:25:48 +0200 Subject: [PATCH 11/30] Update bcdec to latest version --- thirdparty/README.md | 2 +- thirdparty/misc/bcdec.h | 58 ++++++++++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/thirdparty/README.md b/thirdparty/README.md index a219839afca..0bd7c472924 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -652,7 +652,7 @@ Collection of single-file libraries used in Godot components. - `bcdec.h` * Upstream: https://github.com/iOrange/bcdec - * Version: git (026acf98ea271045cb10713daa96ba98528badb7, 2022) + * Version: git (3b29f8f44466c7d59852670f82f53905cf627d48, 2024) * License: MIT - `clipper.{cpp,hpp}` * Upstream: https://sourceforge.net/projects/polyclipping diff --git a/thirdparty/misc/bcdec.h b/thirdparty/misc/bcdec.h index 78074a0c906..275ee05d946 100644 --- a/thirdparty/misc/bcdec.h +++ b/thirdparty/misc/bcdec.h @@ -1,4 +1,4 @@ -/* bcdec.h - v0.96 +/* bcdec.h - v0.97 provides functions to decompress blocks of BC compressed images written by Sergii "iOrange" Kudlai in 2022 @@ -30,6 +30,11 @@ - Split BC6H decompression function into 'half' and 'float' variants + Michael Schmidt (@RunDevelopment) - Found better "magic" coefficients for integer interpolation + of reference colors in BC1 color block, that match with + the floating point interpolation. This also made it faster + than integer division by 3! + bugfixes: @linkmauve @@ -39,6 +44,9 @@ #ifndef BCDEC_HEADER_INCLUDED #define BCDEC_HEADER_INCLUDED +#define BCDEC_VERSION_MAJOR 0 +#define BCDEC_VERSION_MINOR 97 + /* if BCDEC_STATIC causes problems, try defining BCDECDEF to 'inline' or 'static inline' */ #ifndef BCDECDEF #ifdef BCDEC_STATIC @@ -96,6 +104,7 @@ BCDECDEF void bcdec_bc6h_float(const void* compressedBlock, void* decompressedBl BCDECDEF void bcdec_bc6h_half(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +#endif /* BCDEC_HEADER_INCLUDED */ #ifdef BCDEC_IMPLEMENTATION @@ -110,35 +119,44 @@ static void bcdec__color_block(const void* compressedBlock, void* decompressedBl c0 = ((unsigned short*)compressedBlock)[0]; c1 = ((unsigned short*)compressedBlock)[1]; + /* Unpack 565 ref colors */ + r0 = (c0 >> 11) & 0x1F; + g0 = (c0 >> 5) & 0x3F; + b0 = c0 & 0x1F; + + r1 = (c1 >> 11) & 0x1F; + g1 = (c1 >> 5) & 0x3F; + b1 = c1 & 0x1F; + /* Expand 565 ref colors to 888 */ - r0 = (((c0 >> 11) & 0x1F) * 527 + 23) >> 6; - g0 = (((c0 >> 5) & 0x3F) * 259 + 33) >> 6; - b0 = ((c0 & 0x1F) * 527 + 23) >> 6; - refColors[0] = 0xFF000000 | (b0 << 16) | (g0 << 8) | r0; + r = (r0 * 527 + 23) >> 6; + g = (g0 * 259 + 33) >> 6; + b = (b0 * 527 + 23) >> 6; + refColors[0] = 0xFF000000 | (b << 16) | (g << 8) | r; - r1 = (((c1 >> 11) & 0x1F) * 527 + 23) >> 6; - g1 = (((c1 >> 5) & 0x3F) * 259 + 33) >> 6; - b1 = ((c1 & 0x1F) * 527 + 23) >> 6; - refColors[1] = 0xFF000000 | (b1 << 16) | (g1 << 8) | r1; + r = (r1 * 527 + 23) >> 6; + g = (g1 * 259 + 33) >> 6; + b = (b1 * 527 + 23) >> 6; + refColors[1] = 0xFF000000 | (b << 16) | (g << 8) | r; if (c0 > c1 || onlyOpaqueMode) { /* Standard BC1 mode (also BC3 color block uses ONLY this mode) */ /* color_2 = 2/3*color_0 + 1/3*color_1 color_3 = 1/3*color_0 + 2/3*color_1 */ - r = (2 * r0 + r1 + 1) / 3; - g = (2 * g0 + g1 + 1) / 3; - b = (2 * b0 + b1 + 1) / 3; + r = ((2 * r0 + r1) * 351 + 61) >> 7; + g = ((2 * g0 + g1) * 2763 + 1039) >> 11; + b = ((2 * b0 + b1) * 351 + 61) >> 7; refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r; - r = (r0 + 2 * r1 + 1) / 3; - g = (g0 + 2 * g1 + 1) / 3; - b = (b0 + 2 * b1 + 1) / 3; + r = ((r0 + r1 * 2) * 351 + 61) >> 7; + g = ((g0 + g1 * 2) * 2763 + 1039) >> 11; + b = ((b0 + b1 * 2) * 351 + 61) >> 7; refColors[3] = 0xFF000000 | (b << 16) | (g << 8) | r; } else { /* Quite rare BC1A mode */ /* color_2 = 1/2*color_0 + 1/2*color_1; color_3 = 0; */ - r = (r0 + r1 + 1) >> 1; - g = (g0 + g1 + 1) >> 1; - b = (b0 + b1 + 1) >> 1; + r = ((r0 + r1) * 1053 + 125) >> 8; + g = ((g0 + g1) * 4145 + 1019) >> 11; + b = ((b0 + b1) * 1053 + 125) >> 8; refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r; refColors[3] = 0x00000000; @@ -1269,8 +1287,6 @@ BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, in #endif /* BCDEC_IMPLEMENTATION */ -#endif /* BCDEC_HEADER_INCLUDED */ - /* LICENSE: This software is available under 2 licenses -- choose whichever you prefer. @@ -1326,4 +1342,4 @@ OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to -*/ \ No newline at end of file +*/ From ed13a840fa26f813299b8af1cbfb941c8eb95de6 Mon Sep 17 00:00:00 2001 From: chocola-mint <56677134+chocola-mint@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:57:34 +0900 Subject: [PATCH 12/30] Add markers to animation --- doc/classes/Animation.xml | 71 + doc/classes/AnimationPlayer.xml | 89 ++ editor/animation_track_editor.cpp | 1398 ++++++++++++++++- editor/animation_track_editor.h | 216 +++ editor/editor_node.cpp | 1 + editor/icons/Marker.svg | 1 + editor/icons/MarkerSelected.svg | 1 + .../animation_blend_tree_editor_plugin.cpp | 166 ++ .../animation_blend_tree_editor_plugin.h | 47 + .../animation_player_editor_plugin.cpp | 58 +- .../plugins/animation_player_editor_plugin.h | 26 + scene/animation/animation_blend_tree.cpp | 2 + scene/animation/animation_mixer.cpp | 93 +- scene/animation/animation_mixer.h | 2 + scene/animation/animation_player.cpp | 187 ++- scene/animation/animation_player.h | 16 +- scene/resources/animation.cpp | 146 ++ scene/resources/animation.h | 27 + 18 files changed, 2445 insertions(+), 102 deletions(-) create mode 100644 editor/icons/Marker.svg create mode 100644 editor/icons/MarkerSelected.svg diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml index 887e9cda819..609d7eff391 100644 --- a/doc/classes/Animation.xml +++ b/doc/classes/Animation.xml @@ -34,6 +34,14 @@ $DOCS_URL/tutorials/animation/index.html + + + + + + Adds a marker to this Animation. + + @@ -271,12 +279,60 @@ Returns the index of the specified track. If the track is not found, return -1. + + + + + Returns the name of the marker located at the given time. + + + + + + + Returns the given marker's color. + + + + + + Returns every marker in this Animation, sorted ascending by time. + + + + + + + Returns the given marker's time. + + + + + + + Returns the closest marker that comes after the given time. If no such marker exists, an empty string is returned. + + + + + + + Returns the closest marker that comes before the given time. If no such marker exists, an empty string is returned. + + Returns the amount of tracks in the animation. + + + + + Returns [code]true[/code] if this Animation contains a marker with the given name. + + @@ -320,6 +376,13 @@ Returns the interpolated position value at the given time (in seconds). The [param track_idx] must be the index of a 3D position track. + + + + + Removes the marker with the given name from this Animation. + + @@ -363,6 +426,14 @@ Returns the interpolated scale value at the given time (in seconds). The [param track_idx] must be the index of a 3D scale track. + + + + + + Sets the given marker's color. + + diff --git a/doc/classes/AnimationPlayer.xml b/doc/classes/AnimationPlayer.xml index 1ca8ac2fa54..9aeb4b71625 100644 --- a/doc/classes/AnimationPlayer.xml +++ b/doc/classes/AnimationPlayer.xml @@ -75,6 +75,24 @@ Returns the node which node path references will travel from. + + + + Returns the end time of the section currently being played. + + + + + + Returns the start time of the section currently being played. + + + + + + Returns [code]true[/code] if an animation is currently playing with section. + + @@ -110,6 +128,54 @@ This method is a shorthand for [method play] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], so see its description for more information. + + + + + + + + + + Plays the animation with key [param name] and the section starting from [param start_time] and ending on [param end_time]. See also [method play]. + Setting [param start_time] to a value outside the range of the animation means the start of the animation will be used instead, and setting [param end_time] to a value outside the range of the animation means the end of the animation will be used instead. [param start_time] cannot be equal to [param end_time]. + + + + + + + + + + Plays the animation with key [param name] and the section starting from [param start_time] and ending on [param end_time] in reverse. + This method is a shorthand for [method play_section] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], see its description for more information. + + + + + + + + + + + + Plays the animation with key [param name] and the section starting from [param start_marker] and ending on [param end_marker]. + If the start marker is empty, the section starts from the beginning of the animation. If the end marker is empty, the section ends on the end of the animation. See also [method play]. + + + + + + + + + + Plays the animation with key [param name] and the section starting from [param start_marker] and ending on [param end_marker] in reverse. + This method is a shorthand for [method play_section_with_markers] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], see its description for more information. + + @@ -139,6 +205,12 @@ [b]Note:[/b] If a looped animation is currently playing, the queued animation will never play unless the looped animation is stopped somehow. + + + + Resets the current section if section is set. + + @@ -180,6 +252,23 @@ Sets the node which node path references will travel from. + + + + + + Changes the start and end times of the section being played. The current playback position will be clamped within the new section. See also [method play_section]. + + + + + + + + Changes the start and end markers of the section being played. The current playback position will be clamped within the new section. See also [method play_section_with_markers]. + If the argument is empty, the section uses the beginning or end of the animation. If both are empty, it means that the section is not set. + + diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index d277ba2f6d7..63f86607e5a 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -39,6 +39,7 @@ #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/gui/editor_spin_slider.h" +#include "editor/gui/editor_validation_panel.h" #include "editor/gui/scene_tree_editor.h" #include "editor/inspector_dock.h" #include "editor/multi_node_edit.h" @@ -48,6 +49,7 @@ #include "scene/animation/animation_player.h" #include "scene/animation/tween.h" #include "scene/gui/check_box.h" +#include "scene/gui/color_picker.h" #include "scene/gui/grid_container.h" #include "scene/gui/option_button.h" #include "scene/gui/panel_container.h" @@ -1467,7 +1469,7 @@ void AnimationTimelineEdit::_notification(int p_what) { case NOTIFICATION_DRAW: { int key_range = get_size().width - get_buttons_width() - get_name_limit(); - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -1522,6 +1524,18 @@ void AnimationTimelineEdit::_notification(int p_what) { } } + PackedStringArray markers = animation->get_marker_names(); + if (markers.size() > 0) { + float min_marker = animation->get_marker_time(markers[0]); + float max_marker = animation->get_marker_time(markers[markers.size() - 1]); + if (min_marker < time_min) { + time_min = min_marker; + } + if (max_marker > time_max) { + time_max = max_marker; + } + } + float extra = (zoomw / scale) * 0.5; time_max += extra; @@ -1701,7 +1715,7 @@ void AnimationTimelineEdit::set_zoom(Range *p_zoom) { } void AnimationTimelineEdit::auto_fit() { - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -1780,7 +1794,7 @@ void AnimationTimelineEdit::update_play_position() { } void AnimationTimelineEdit::update_values() { - if (!animation.is_valid() || editing) { + if (animation.is_null() || editing) { return; } @@ -1792,6 +1806,7 @@ void AnimationTimelineEdit::update_values() { time_icon->set_tooltip_text(TTR("Animation length (frames)")); if (track_edit) { track_edit->editor->_update_key_edit(); + track_edit->editor->marker_edit->_update_key_edit(); } } else { length->set_value(animation->get_length()); @@ -1821,7 +1836,7 @@ void AnimationTimelineEdit::update_values() { } void AnimationTimelineEdit::_play_position_draw() { - if (!animation.is_valid() || play_position_pos < 0) { + if (animation.is_null() || play_position_pos < 0) { return; } @@ -1972,6 +1987,7 @@ AnimationTimelineEdit::AnimationTimelineEdit() { Control *expander = memnew(Control); expander->set_h_size_flags(SIZE_EXPAND_FILL); + expander->set_mouse_filter(MOUSE_FILTER_IGNORE); len_hb->add_child(expander); time_icon = memnew(TextureRect); time_icon->set_v_size_flags(SIZE_SHRINK_CENTER); @@ -2124,6 +2140,62 @@ void AnimationTrackEdit::_notification(int p_what) { draw_line(Point2(limit, 0), Point2(limit, get_size().height), h_line_color, Math::round(EDSCALE)); } + // Marker sections. + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray section = editor->get_selected_section(); + if (section.size() == 2) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + double start_time = animation->get_marker_time(start_marker); + double end_time = animation->get_marker_time(end_marker); + + // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (editor->is_marker_moving_selection() && !(player && player->is_playing())) { + start_time += editor->get_marker_moving_selection_offset(); + end_time += editor->get_marker_moving_selection_offset(); + } + + if (start_time < animation->get_length() && end_time >= 0) { + float start_ofs = MAX(0, start_time) - timeline->get_value(); + float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value(); + start_ofs = start_ofs * scale + limit; + end_ofs = end_ofs * scale + limit; + start_ofs = MAX(start_ofs, limit); + end_ofs = MIN(end_ofs, limit_end); + Rect2 rect; + rect.set_position(Vector2(start_ofs, 0)); + rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); + + draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); + } + } + } + + // Marker overlays. + + { + float scale = timeline->get_zoom_scale(); + PackedStringArray markers = animation->get_marker_names(); + for (const StringName marker : markers) { + double time = animation->get_marker_time(marker); + if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) { + time += editor->get_marker_moving_selection_offset(); + } + if (time >= 0) { + float offset = time - timeline->get_value(); + offset = offset * scale + limit; + Color marker_color = animation->get_marker_color(marker); + marker_color.a = 0.2; + draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color); + } + } + } + // Keyframes. draw_bg(limit, get_size().width - timeline->get_buttons_width() - outer_margin); @@ -2352,7 +2424,7 @@ void AnimationTrackEdit::_notification(int p_what) { } int AnimationTrackEdit::get_key_height() const { - if (!animation.is_valid()) { + if (animation.is_null()) { return 0; } @@ -2360,7 +2432,7 @@ int AnimationTrackEdit::get_key_height() const { } Rect2 AnimationTrackEdit::get_key_rect(int p_index, float p_pixels_sec) { - if (!animation.is_valid()) { + if (animation.is_null()) { return Rect2(); } Rect2 rect = Rect2(-type_icon->get_width() / 2, 0, type_icon->get_width(), get_size().height); @@ -2399,7 +2471,7 @@ void AnimationTrackEdit::draw_key_link(int p_index, float p_pixels_sec, int p_x, } void AnimationTrackEdit::draw_key(int p_index, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) { - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -2573,7 +2645,7 @@ void AnimationTrackEdit::set_editor(AnimationTrackEditor *p_editor) { } void AnimationTrackEdit::_play_position_draw() { - if (!animation.is_valid() || play_position_pos < 0) { + if (animation.is_null() || play_position_pos < 0) { return; } @@ -3522,6 +3594,64 @@ void AnimationTrackEditGroup::_notification(int p_what) { draw_style_box(stylebox_header, Rect2(Point2(), get_size())); + int limit = timeline->get_name_limit(); + + // Section preview. + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray section = editor->get_selected_section(); + if (section.size() == 2) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + double start_time = editor->get_current_animation()->get_marker_time(start_marker); + double end_time = editor->get_current_animation()->get_marker_time(end_marker); + + // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (editor->is_marker_moving_selection() && !(player && player->is_playing())) { + start_time += editor->get_marker_moving_selection_offset(); + end_time += editor->get_marker_moving_selection_offset(); + } + + if (start_time < editor->get_current_animation()->get_length() && end_time >= 0) { + float start_ofs = MAX(0, start_time) - timeline->get_value(); + float end_ofs = MIN(editor->get_current_animation()->get_length(), end_time) - timeline->get_value(); + start_ofs = start_ofs * scale + limit; + end_ofs = end_ofs * scale + limit; + start_ofs = MAX(start_ofs, limit); + end_ofs = MIN(end_ofs, limit_end); + Rect2 rect; + rect.set_position(Vector2(start_ofs, 0)); + rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); + + draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); + } + } + } + + // Marker overlays. + + { + float scale = timeline->get_zoom_scale(); + PackedStringArray markers = editor->get_current_animation()->get_marker_names(); + for (const StringName marker : markers) { + double time = editor->get_current_animation()->get_marker_time(marker); + if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) { + time += editor->get_marker_moving_selection_offset(); + } + if (time >= 0) { + float offset = time - timeline->get_value(); + offset = offset * scale + limit; + Color marker_color = editor->get_current_animation()->get_marker_color(marker); + marker_color.a = 0.2; + draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color); + } + } + } + draw_line(Point2(), Point2(get_size().width, 0), h_line_color, Math::round(EDSCALE)); draw_line(Point2(timeline->get_name_limit(), 0), Point2(timeline->get_name_limit(), get_size().height), v_line_color, Math::round(EDSCALE)); draw_line(Point2(get_size().width - timeline->get_buttons_width() - outer_margin, 0), Point2(get_size().width - timeline->get_buttons_width() - outer_margin, get_size().height), v_line_color, Math::round(EDSCALE)); @@ -3590,6 +3720,10 @@ void AnimationTrackEditGroup::set_root(Node *p_root) { queue_redraw(); } +void AnimationTrackEditGroup::set_editor(AnimationTrackEditor *p_editor) { + editor = p_editor; +} + void AnimationTrackEditGroup::_zoom_changed() { queue_redraw(); } @@ -3623,6 +3757,9 @@ void AnimationTrackEditor::set_animation(const Ref &p_anim, bool p_re read_only = p_read_only; timeline->set_animation(p_anim, read_only); + marker_edit->set_animation(p_anim, read_only); + marker_edit->set_play_position(timeline->get_play_position()); + _cancel_bezier_edit(); _update_tracks(); @@ -3873,6 +4010,7 @@ void AnimationTrackEditor::_track_grab_focus(int p_track) { void AnimationTrackEditor::set_anim_pos(float p_pos) { timeline->set_play_position(p_pos); + marker_edit->set_play_position(p_pos); for (int i = 0; i < track_edits.size(); i++) { track_edits[i]->set_play_position(p_pos); } @@ -4043,7 +4181,7 @@ void AnimationTrackEditor::insert_transform_key(Node3D *p_node, const String &p_ if (!keying) { return; } - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -4083,7 +4221,7 @@ bool AnimationTrackEditor::has_track(Node3D *p_node, const String &p_sub, const if (!keying) { return false; } - if (!animation.is_valid()) { + if (animation.is_null()) { return false; } @@ -4230,6 +4368,22 @@ void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_p _query_insert(id); } +PackedStringArray AnimationTrackEditor::get_selected_section() const { + return marker_edit->get_selected_section(); +} + +bool AnimationTrackEditor::is_marker_selected(const StringName &p_marker) const { + return marker_edit->is_marker_selected(p_marker); +} + +bool AnimationTrackEditor::is_marker_moving_selection() const { + return marker_edit->is_moving_selection(); +} + +float AnimationTrackEditor::get_marker_moving_selection_offset() const { + return marker_edit->get_moving_selection_offset(); +} + void AnimationTrackEditor::insert_value_key(const String &p_property, bool p_advance) { EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history(); @@ -4316,7 +4470,7 @@ void AnimationTrackEditor::_confirm_insert_list() { PropertyInfo AnimationTrackEditor::_find_hint_for_track(int p_idx, NodePath &r_base_path, Variant *r_current_val) { r_base_path = NodePath(); - ERR_FAIL_COND_V(!animation.is_valid(), PropertyInfo()); + ERR_FAIL_COND_V(animation.is_null(), PropertyInfo()); ERR_FAIL_INDEX_V(p_idx, animation->get_track_count(), PropertyInfo()); if (!root) { @@ -4769,6 +4923,7 @@ void AnimationTrackEditor::_update_tracks() { g->set_root(root); g->set_tooltip_text(tooltip); g->set_timeline(timeline); + g->set_editor(this); groups.push_back(g); VBoxContainer *vb = memnew(VBoxContainer); vb->add_theme_constant_override("separation", 0); @@ -4860,12 +5015,13 @@ void AnimationTrackEditor::_snap_mode_changed(int p_mode) { if (key_edit) { key_edit->set_use_fps(use_fps); } + marker_edit->set_use_fps(use_fps); step->set_step(use_fps ? FPS_DECIMAL : SECOND_DECIMAL); _update_step_spinbox(); } void AnimationTrackEditor::_update_step_spinbox() { - if (!animation.is_valid()) { + if (animation.is_null()) { return; } step->set_block_signals(true); @@ -4978,6 +5134,7 @@ void AnimationTrackEditor::_notification(int p_what) { void AnimationTrackEditor::_update_scroll(double) { _redraw_tracks(); _redraw_groups(); + marker_edit->queue_redraw(); } void AnimationTrackEditor::_update_step(double p_new_step) { @@ -5253,6 +5410,8 @@ void AnimationTrackEditor::_timeline_value_changed(double) { bezier_edit->queue_redraw(); bezier_edit->update_play_position(); + + marker_edit->update_play_position(); } int AnimationTrackEditor::_get_track_selected() { @@ -5445,6 +5604,8 @@ void AnimationTrackEditor::_key_selected(int p_key, bool p_single, int p_track) _redraw_tracks(); _update_key_edit(); + + marker_edit->_clear_selection(marker_edit->is_selection_active()); } void AnimationTrackEditor::_key_deselected(int p_key, int p_track) { @@ -5513,7 +5674,7 @@ void AnimationTrackEditor::_clear_selection(bool p_update) { void AnimationTrackEditor::_update_key_edit() { _clear_key_edit(); - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -5600,6 +5761,8 @@ void AnimationTrackEditor::_select_at_anim(const Ref &p_anim, int p_t selection.insert(sk, ki); _update_key_edit(); + + marker_edit->_clear_selection(marker_edit->is_selection_active()); } void AnimationTrackEditor::_move_selection_commit() { @@ -7311,6 +7474,15 @@ AnimationTrackEditor::AnimationTrackEditor() { box_selection_container->set_clip_contents(true); timeline_vbox->add_child(box_selection_container); + marker_edit = memnew(AnimationMarkerEdit); + timeline->get_child(0)->add_child(marker_edit); + marker_edit->set_editor(this); + marker_edit->set_timeline(timeline); + marker_edit->set_h_size_flags(SIZE_EXPAND_FILL); + marker_edit->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT); + marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_groups)); + marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_tracks)); + scroll = memnew(ScrollContainer); box_selection_container->add_child(scroll); scroll->set_anchors_and_offsets_preset(PRESET_FULL_RECT); @@ -7826,3 +7998,1203 @@ AnimationTrackKeyEditEditor::AnimationTrackKeyEditEditor(Ref p_animat AnimationTrackKeyEditEditor::~AnimationTrackKeyEditEditor() { } + +void AnimationMarkerEdit::_zoom_changed() { + queue_redraw(); + play_position->queue_redraw(); +} + +void AnimationMarkerEdit::_menu_selected(int p_index) { + switch (p_index) { + case MENU_KEY_INSERT: { + _insert_marker(insert_at_pos); + } break; + case MENU_KEY_RENAME: { + if (selection.size() > 0) { + _rename_marker(*selection.last()); + } + } break; + case MENU_KEY_DELETE: { + _delete_selected_markers(); + } break; + case MENU_KEY_TOGGLE_MARKER_NAMES: { + should_show_all_marker_names = !should_show_all_marker_names; + queue_redraw(); + } break; + } +} + +void AnimationMarkerEdit::_play_position_draw() { + if (animation.is_null() || play_position_pos < 0) { + return; + } + + float scale = timeline->get_zoom_scale(); + int h = get_size().height; + + int px = (play_position_pos - timeline->get_value()) * scale + timeline->get_name_limit(); + + if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) { + Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); + play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE)); + } +} + +bool AnimationMarkerEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) { + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + int limit_start_hitbox = limit - type_icon->get_width(); + + if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) { + int key_idx = -1; + float key_distance = 1e20; + PackedStringArray names = animation->get_marker_names(); + for (int i = 0; i < names.size(); i++) { + Rect2 rect = const_cast(this)->get_key_rect(timeline->get_zoom_scale()); + float offset = animation->get_marker_time(names[i]) - timeline->get_value(); + offset = offset * timeline->get_zoom_scale() + limit; + rect.position.x += offset; + if (rect.has_point(p_pos)) { + if (const_cast(this)->is_key_selectable_by_distance()) { + float distance = Math::abs(offset - p_pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + } + } else { + // First one does it. + break; + } + } + } + + if (key_idx != -1) { + if (p_aggregate) { + StringName name = names[key_idx]; + if (selection.has(name)) { + if (p_deselectable) { + call_deferred("_deselect_key", name); + moving_selection_pivot = 0.0f; + moving_selection_mouse_begin_x = 0.0f; + } + } else { + call_deferred("_select_key", name, false); + moving_selection_attempt = true; + moving_selection_effective = false; + select_single_attempt = StringName(); + moving_selection_pivot = animation->get_marker_time(name); + moving_selection_mouse_begin_x = p_pos.x; + } + + } else { + StringName name = names[key_idx]; + if (!selection.has(name)) { + call_deferred("_select_key", name, true); + select_single_attempt = StringName(); + } else { + select_single_attempt = name; + } + + moving_selection_attempt = true; + moving_selection_effective = false; + moving_selection_pivot = animation->get_marker_time(name); + moving_selection_mouse_begin_x = p_pos.x; + } + + if (read_only) { + moving_selection_attempt = false; + moving_selection_pivot = 0.0f; + moving_selection_mouse_begin_x = 0.0f; + } + return true; + } + } + + return false; +} + +bool AnimationMarkerEdit::_is_ui_pos_in_current_section(const Point2 &p_pos) { + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + if (p_pos.x >= limit && p_pos.x <= limit_end) { + PackedStringArray section = get_selected_section(); + if (!section.is_empty()) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + float start_offset = (animation->get_marker_time(start_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit; + float end_offset = (animation->get_marker_time(end_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit; + return p_pos.x >= start_offset && p_pos.x <= end_offset; + } + } + + return false; +} + +HBoxContainer *AnimationMarkerEdit::_create_hbox_labeled_control(const String &p_text, Control *p_control) const { + HBoxContainer *hbox = memnew(HBoxContainer); + Label *label = memnew(Label); + label->set_text(p_text); + hbox->add_child(label); + hbox->add_child(p_control); + hbox->set_h_size_flags(SIZE_EXPAND_FILL); + label->set_h_size_flags(SIZE_EXPAND_FILL); + label->set_stretch_ratio(1.0); + p_control->set_h_size_flags(SIZE_EXPAND_FILL); + p_control->set_stretch_ratio(1.0); + return hbox; +} + +void AnimationMarkerEdit::_update_key_edit() { + _clear_key_edit(); + if (animation.is_null()) { + return; + } + + if (selection.size() == 1) { + key_edit = memnew(AnimationMarkerKeyEdit); + key_edit->animation = animation; + key_edit->animation_read_only = read_only; + key_edit->marker_name = *selection.begin(); + key_edit->use_fps = timeline->is_using_fps(); + key_edit->marker_edit = this; + + EditorNode::get_singleton()->push_item(key_edit); + + InspectorDock::get_singleton()->set_info(TTR("Marker name is read-only in the inspector."), TTR("A marker's name can only be changed by right-clicking it in the animation editor and selecting \"Rename Marker\", in order to make sure that marker names are all unique."), true); + } else if (selection.size() > 1) { + multi_key_edit = memnew(AnimationMultiMarkerKeyEdit); + multi_key_edit->animation = animation; + multi_key_edit->animation_read_only = read_only; + multi_key_edit->marker_edit = this; + for (const StringName &name : selection) { + multi_key_edit->marker_names.push_back(name); + } + + EditorNode::get_singleton()->push_item(multi_key_edit); + } +} + +void AnimationMarkerEdit::_clear_key_edit() { + if (key_edit) { + // If key edit is the object being inspected, remove it first. + if (InspectorDock::get_inspector_singleton()->get_edited_object() == key_edit) { + EditorNode::get_singleton()->push_item(nullptr); + } + + // Then actually delete it. + memdelete(key_edit); + key_edit = nullptr; + } + + if (multi_key_edit) { + if (InspectorDock::get_inspector_singleton()->get_edited_object() == multi_key_edit) { + EditorNode::get_singleton()->push_item(nullptr); + } + + memdelete(multi_key_edit); + multi_key_edit = nullptr; + } +} + +void AnimationMarkerEdit::_bind_methods() { + ClassDB::bind_method("_clear_selection_for_anim", &AnimationMarkerEdit::_clear_selection_for_anim); + ClassDB::bind_method("_select_key", &AnimationMarkerEdit::_select_key); + ClassDB::bind_method("_deselect_key", &AnimationMarkerEdit::_deselect_key); +} + +void AnimationMarkerEdit::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + if (animation.is_null()) { + return; + } + + type_icon = get_editor_theme_icon(SNAME("Marker")); + selected_icon = get_editor_theme_icon(SNAME("MarkerSelected")); + } break; + + case NOTIFICATION_DRAW: { + if (animation.is_null()) { + return; + } + + int limit = timeline->get_name_limit(); + + Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); + Color color = get_theme_color(SceneStringName(font_color), SNAME("Label")); + int hsep = get_theme_constant(SNAME("h_separation"), SNAME("ItemList")); + Color linecolor = color; + linecolor.a = 0.2; + + // SECTION PREVIEW // + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray section = get_selected_section(); + if (section.size() == 2) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + double start_time = animation->get_marker_time(start_marker); + double end_time = animation->get_marker_time(end_marker); + + // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (moving_selection && !(player && player->is_playing())) { + start_time += moving_selection_offset; + end_time += moving_selection_offset; + } + + if (start_time < animation->get_length() && end_time >= 0) { + float start_ofs = MAX(0, start_time) - timeline->get_value(); + float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value(); + start_ofs = start_ofs * scale + limit; + end_ofs = end_ofs * scale + limit; + start_ofs = MAX(start_ofs, limit); + end_ofs = MIN(end_ofs, limit_end); + Rect2 rect; + rect.set_position(Vector2(start_ofs, 0)); + rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); + + draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); + } + } + } + + // KEYFRAMES // + + draw_bg(limit, get_size().width - timeline->get_buttons_width()); + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray names = animation->get_marker_names(); + for (int i = 0; i < names.size(); i++) { + StringName name = names[i]; + bool is_selected = selection.has(name); + float offset = animation->get_marker_time(name) - timeline->get_value(); + if (is_selected && moving_selection) { + offset += moving_selection_offset; + } + + offset = offset * scale + limit; + + draw_key(name, scale, int(offset), is_selected, limit, limit_end); + + const int font_size = 16; + Size2 string_size = font->get_string_size(name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size); + if (int(offset) <= limit_end && int(offset) >= limit && should_show_all_marker_names) { + float bottom = get_size().height + string_size.y - font->get_descent(font_size); + float extrusion = MAX(0, offset + string_size.x - limit_end); // How much the string would extrude outside limit_end if unadjusted. + Color marker_color = animation->get_marker_color(name); + draw_string(font, Point2(offset - extrusion, bottom), name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, marker_color); + draw_string_outline(font, Point2(offset - extrusion, bottom), name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, 1, color); + } + } + } + + draw_fg(limit, get_size().width - timeline->get_buttons_width()); + + // BUTTONS // + + { + int ofs = get_size().width - timeline->get_buttons_width(); + + draw_line(Point2(ofs, 0), Point2(ofs, get_size().height), linecolor, Math::round(EDSCALE)); + + ofs += hsep; + } + + draw_line(Vector2(0, get_size().height), get_size(), linecolor, Math::round(EDSCALE)); + } break; + + case NOTIFICATION_MOUSE_ENTER: + hovered = true; + queue_redraw(); + break; + case NOTIFICATION_MOUSE_EXIT: + hovered = false; + // When the mouse cursor exits the track, we're no longer hovering any keyframe. + hovering_marker = StringName(); + queue_redraw(); + break; + } +} + +void AnimationMarkerEdit::gui_input(const Ref &p_event) { + ERR_FAIL_COND(p_event.is_null()); + + if (animation.is_null()) { + return; + } + + if (p_event->is_pressed()) { + if (ED_IS_SHORTCUT("animation_marker_edit/rename_marker", p_event)) { + if (!read_only) { + _menu_selected(MENU_KEY_RENAME); + } + } + + if (ED_IS_SHORTCUT("animation_marker_edit/delete_selection", p_event)) { + if (!read_only) { + _menu_selected(MENU_KEY_DELETE); + } + } + + if (ED_IS_SHORTCUT("animation_marker_edit/toggle_marker_names", p_event)) { + if (!read_only) { + _menu_selected(MENU_KEY_TOGGLE_MARKER_NAMES); + } + } + } + + Ref mb = p_event; + + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + Point2 pos = mb->get_position(); + if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) { + accept_event(); + } else if (!_is_ui_pos_in_current_section(pos)) { + _clear_selection_for_anim(animation); + } + } + + if (mb.is_valid() && moving_selection_attempt) { + if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + moving_selection_attempt = false; + if (moving_selection && moving_selection_effective) { + if (Math::abs(moving_selection_offset) > CMP_EPSILON) { + _move_selection_commit(); + accept_event(); // So play position doesn't snap to the end of move selection. + } + } else if (select_single_attempt) { + call_deferred("_select_key", select_single_attempt, true); + + // First select click should not affect play position. + if (!selection.has(select_single_attempt)) { + accept_event(); + } else { + // Second click and onwards should snap to marker time. + double ofs = animation->get_marker_time(select_single_attempt); + timeline->set_play_position(ofs); + timeline->emit_signal(SNAME("timeline_changed"), ofs, mb->is_alt_pressed()); + accept_event(); + } + } else { + // First select click should not affect play position. + if (!selection.has(select_single_attempt)) { + accept_event(); + } + } + + moving_selection = false; + select_single_attempt = StringName(); + } + + if (moving_selection && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { + moving_selection_attempt = false; + moving_selection = false; + _move_selection_cancel(); + } + } + + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { + Point2 pos = mb->get_position(); + if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) { + // Can do something with menu too! show insert key. + float offset = (pos.x - timeline->get_name_limit()) / timeline->get_zoom_scale(); + if (!read_only) { + bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false); + + menu->clear(); + menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Marker..."), MENU_KEY_INSERT); + + if (selected || selection.size() > 0) { + menu->add_icon_item(get_editor_theme_icon(SNAME("Edit")), TTR("Rename Marker"), MENU_KEY_RENAME); + menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Marker(s)"), MENU_KEY_DELETE); + } + + menu->add_icon_item(get_editor_theme_icon(should_show_all_marker_names ? SNAME("GuiChecked") : SNAME("GuiUnchecked")), TTR("Show All Marker Names"), MENU_KEY_TOGGLE_MARKER_NAMES); + menu->reset_size(); + + moving_selection_attempt = false; + moving_selection = false; + + menu->set_position(get_screen_position() + get_local_mouse_position()); + menu->popup(); + + insert_at_pos = offset + timeline->get_value(); + accept_event(); + } + } + } + + Ref mm = p_event; + + if (mm.is_valid()) { + const StringName previous_hovering_marker = hovering_marker; + + // Hovering compressed keyframes for editing is not possible. + const float scale = timeline->get_zoom_scale(); + const int limit = timeline->get_name_limit(); + const int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + const int limit_start_hitbox = limit - type_icon->get_width(); + const Point2 pos = mm->get_position(); + + if (pos.x >= limit_start_hitbox && pos.x <= limit_end) { + // Use the same logic as key selection to ensure that hovering accurately represents + // which key will be selected when clicking. + int key_idx = -1; + float key_distance = 1e20; + + hovering_marker = StringName(); + + PackedStringArray names = animation->get_marker_names(); + + // Hovering should happen in the opposite order of drawing for more accurate overlap hovering. + for (int i = names.size() - 1; i >= 0; i--) { + StringName name = names[i]; + Rect2 rect = get_key_rect(scale); + float offset = animation->get_marker_time(name) - timeline->get_value(); + offset = offset * scale + limit; + rect.position.x += offset; + + if (rect.has_point(pos)) { + if (is_key_selectable_by_distance()) { + const float distance = Math::abs(offset - pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + hovering_marker = name; + } + } else { + // First one does it. + hovering_marker = name; + break; + } + } + } + + if (hovering_marker != previous_hovering_marker) { + // Required to draw keyframe hover feedback on the correct keyframe. + queue_redraw(); + } + } + } + + if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && moving_selection_attempt) { + if (!moving_selection) { + moving_selection = true; + _move_selection_begin(); + } + + float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); + float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); + float delta = new_time - moving_begin_time; + float snapped_time = editor->snap_time(moving_selection_pivot + delta); + + float offset = 0.0; + if (Math::abs(editor->get_moving_selection_offset()) > CMP_EPSILON || (snapped_time > moving_selection_pivot && delta > CMP_EPSILON) || (snapped_time < moving_selection_pivot && delta < -CMP_EPSILON)) { + offset = snapped_time - moving_selection_pivot; + moving_selection_effective = true; + } + + _move_selection(offset); + } +} + +String AnimationMarkerEdit::get_tooltip(const Point2 &p_pos) const { + if (animation.is_null()) { + return Control::get_tooltip(p_pos); + } + + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + int limit_start_hitbox = limit - type_icon->get_width(); + + if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) { + int key_idx = -1; + float key_distance = 1e20; + + PackedStringArray names = animation->get_marker_names(); + + // Select should happen in the opposite order of drawing for more accurate overlap select. + for (int i = names.size() - 1; i >= 0; i--) { + StringName name = names[i]; + Rect2 rect = const_cast(this)->get_key_rect(timeline->get_zoom_scale()); + float offset = animation->get_marker_time(name) - timeline->get_value(); + offset = offset * timeline->get_zoom_scale() + limit; + rect.position.x += offset; + + if (rect.has_point(p_pos)) { + if (const_cast(this)->is_key_selectable_by_distance()) { + float distance = ABS(offset - p_pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + } + } else { + // First one does it. + break; + } + } + } + + if (key_idx != -1) { + String name = names[key_idx]; + String text = TTR("Time (s):") + " " + TS->format_number(rtos(Math::snapped(animation->get_marker_time(name), 0.0001))) + "\n"; + text += TTR("Marker:") + " " + name + "\n"; + return text; + } + } + + return Control::get_tooltip(p_pos); +} + +int AnimationMarkerEdit::get_key_height() const { + if (animation.is_null()) { + return 0; + } + + return type_icon->get_height(); +} + +Rect2 AnimationMarkerEdit::get_key_rect(float p_pixels_sec) const { + if (animation.is_null()) { + return Rect2(); + } + + Rect2 rect = Rect2(-type_icon->get_width() / 2, get_size().height - type_icon->get_size().height, type_icon->get_width(), type_icon->get_size().height); + + // Make it a big easier to click. + rect.position.x -= rect.size.x * 0.5; + rect.size.x *= 2; + return rect; +} + +PackedStringArray AnimationMarkerEdit::get_selected_section() const { + if (selection.size() >= 2) { + PackedStringArray arr; + arr.push_back(""); // Marker with smallest time. + arr.push_back(""); // Marker with largest time. + double min_time = INFINITY; + double max_time = -INFINITY; + for (const StringName &marker_name : selection) { + double time = animation->get_marker_time(marker_name); + if (time < min_time) { + arr.set(0, marker_name); + min_time = time; + } + if (time > max_time) { + arr.set(1, marker_name); + max_time = time; + } + } + return arr; + } + + return PackedStringArray(); +} + +bool AnimationMarkerEdit::is_marker_selected(const StringName &p_marker) const { + return selection.has(p_marker); +} + +bool AnimationMarkerEdit::is_key_selectable_by_distance() const { + return true; +} + +void AnimationMarkerEdit::draw_key(const StringName &p_name, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) { + if (animation.is_null()) { + return; + } + + if (p_x < p_clip_left || p_x > p_clip_right) { + return; + } + + Ref icon_to_draw = p_selected ? selected_icon : type_icon; + + Vector2 ofs(p_x - icon_to_draw->get_width() / 2, int(get_size().height - icon_to_draw->get_height())); + + // Don't apply custom marker color when the key is selected. + Color marker_color = p_selected ? Color(1, 1, 1) : animation->get_marker_color(p_name); + + // Use a different color for the currently hovered key. + // The color multiplier is chosen to work with both dark and light editor themes, + // and on both unselected and selected key icons. + draw_texture( + icon_to_draw, + ofs, + p_name == hovering_marker ? get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")) : marker_color); +} + +void AnimationMarkerEdit::draw_bg(int p_clip_left, int p_clip_right) { +} + +void AnimationMarkerEdit::draw_fg(int p_clip_left, int p_clip_right) { +} + +Ref AnimationMarkerEdit::get_animation() const { + return animation; +} + +void AnimationMarkerEdit::set_animation(const Ref &p_animation, bool p_read_only) { + if (animation.is_valid()) { + _clear_selection_for_anim(animation); + } + animation = p_animation; + read_only = p_read_only; + type_icon = get_editor_theme_icon(SNAME("Marker")); + selected_icon = get_editor_theme_icon(SNAME("MarkerSelected")); + + queue_redraw(); +} + +Size2 AnimationMarkerEdit::get_minimum_size() const { + Ref texture = get_editor_theme_icon(SNAME("Object")); + Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); + int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); + int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList")); + + int max_h = MAX(texture->get_height(), font->get_height(font_size)); + max_h = MAX(max_h, get_key_height()); + + return Vector2(1, max_h + separation); +} + +void AnimationMarkerEdit::set_timeline(AnimationTimelineEdit *p_timeline) { + timeline = p_timeline; + timeline->connect("zoom_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed)); + timeline->connect("name_limit_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed)); +} + +void AnimationMarkerEdit::set_editor(AnimationTrackEditor *p_editor) { + editor = p_editor; +} + +void AnimationMarkerEdit::set_play_position(float p_pos) { + play_position_pos = p_pos; + play_position->queue_redraw(); +} + +void AnimationMarkerEdit::update_play_position() { + play_position->queue_redraw(); +} + +void AnimationMarkerEdit::set_use_fps(bool p_use_fps) { + if (key_edit) { + key_edit->use_fps = p_use_fps; + key_edit->notify_property_list_changed(); + } +} + +void AnimationMarkerEdit::_move_selection_begin() { + moving_selection = true; + moving_selection_offset = 0; +} + +void AnimationMarkerEdit::_move_selection(float p_offset) { + moving_selection_offset = p_offset; + queue_redraw(); +} + +void AnimationMarkerEdit::_move_selection_commit() { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Move Markers")); + + for (HashSet::Iterator E = selection.last(); E; --E) { + StringName name = *E; + double time = animation->get_marker_time(name); + float newpos = time + moving_selection_offset; + undo_redo->add_do_method(animation.ptr(), "remove_marker", name); + undo_redo->add_do_method(animation.ptr(), "add_marker", name, newpos); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name)); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name)); + + // add_marker will overwrite the overlapped key on the redo pass, so we add it back on the undo pass. + if (StringName overlap = animation->get_marker_at_time(newpos)) { + if (select_single_attempt == overlap) { + select_single_attempt = ""; + } + undo_redo->add_undo_method(animation.ptr(), "add_marker", overlap, newpos); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", overlap, animation->get_marker_color(overlap)); + } + } + + moving_selection = false; + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + PackedStringArray selected_section = get_selected_section(); + if (selected_section.size() >= 2) { + undo_redo->add_do_method(player, "set_section_with_markers", selected_section[0], selected_section[1]); + undo_redo->add_undo_method(player, "set_section_with_markers", selected_section[0], selected_section[1]); + } + } + undo_redo->add_do_method(timeline, "queue_redraw"); + undo_redo->add_undo_method(timeline, "queue_redraw"); + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + undo_redo->commit_action(); + _update_key_edit(); +} + +void AnimationMarkerEdit::_delete_selected_markers() { + if (selection.size()) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Delete Keys")); + for (const StringName &name : selection) { + double time = animation->get_marker_time(name); + undo_redo->add_do_method(animation.ptr(), "remove_marker", name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name)); + } + _clear_selection_for_anim(animation); + + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + undo_redo->commit_action(); + _update_key_edit(); + } +} + +void AnimationMarkerEdit::_move_selection_cancel() { + moving_selection = false; + queue_redraw(); +} + +void AnimationMarkerEdit::_clear_selection(bool p_update) { + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + player->reset_section(); + } + + selection.clear(); + + if (p_update) { + queue_redraw(); + } + + _clear_key_edit(); +} + +void AnimationMarkerEdit::_clear_selection_for_anim(const Ref &p_anim) { + if (animation != p_anim) { + return; + } + + _clear_selection(true); +} + +void AnimationMarkerEdit::_select_key(const StringName &p_name, bool is_single) { + if (is_single) { + _clear_selection(false); + } + + selection.insert(p_name); + + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + if (selection.size() >= 2) { + PackedStringArray selected_section = get_selected_section(); + double start_time = animation->get_marker_time(selected_section[0]); + double end_time = animation->get_marker_time(selected_section[1]); + player->set_section(start_time, end_time); + } else { + player->reset_section(); + } + } + + queue_redraw(); + _update_key_edit(); + + editor->_clear_selection(editor->is_selection_active()); +} + +void AnimationMarkerEdit::_deselect_key(const StringName &p_name) { + selection.erase(p_name); + + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + if (selection.size() >= 2) { + PackedStringArray selected_section = get_selected_section(); + double start_time = animation->get_marker_time(selected_section[0]); + double end_time = animation->get_marker_time(selected_section[1]); + player->set_section(start_time, end_time); + } else { + player->reset_section(); + } + } + + queue_redraw(); + _update_key_edit(); +} + +void AnimationMarkerEdit::_insert_marker(float p_ofs) { + if (editor->is_snap_timeline_enabled()) { + p_ofs = editor->snap_time(p_ofs); + } + + marker_insert_confirm->popup_centered(Size2(200, 100) * EDSCALE); + marker_insert_color->set_pick_color(Color(1, 1, 1)); + + String base = "new_marker"; + int count = 1; + while (true) { + String attempt = base; + if (count > 1) { + attempt += vformat("_%d", count); + } + if (animation->has_marker(attempt)) { + count++; + continue; + } + base = attempt; + break; + } + + marker_insert_new_name->set_text(base); + _marker_insert_new_name_changed(base); + marker_insert_ofs = p_ofs; +} + +void AnimationMarkerEdit::_rename_marker(const StringName &p_name) { + marker_rename_confirm->popup_centered(Size2i(200, 0) * EDSCALE); + marker_rename_prev_name = p_name; + marker_rename_new_name->set_text(p_name); +} + +void AnimationMarkerEdit::_marker_insert_confirmed() { + StringName name = marker_insert_new_name->get_text(); + + if (animation->has_marker(name)) { + marker_insert_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), name)); + marker_insert_error_dialog->popup_centered(); + return; + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + undo_redo->create_action(TTR("Add Marker Key")); + undo_redo->add_do_method(animation.ptr(), "add_marker", name, marker_insert_ofs); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", name); + StringName existing_marker = animation->get_marker_at_time(marker_insert_ofs); + if (existing_marker) { + undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, marker_insert_ofs); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker)); + } + undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, marker_insert_color->get_pick_color()); + + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + + undo_redo->commit_action(); + + marker_insert_confirm->hide(); +} + +void AnimationMarkerEdit::_marker_insert_new_name_changed(const String &p_text) { + marker_insert_confirm->get_ok_button()->set_disabled(p_text.is_empty()); +} + +void AnimationMarkerEdit::_marker_rename_confirmed() { + StringName new_name = marker_rename_new_name->get_text(); + StringName prev_name = marker_rename_prev_name; + + if (new_name == StringName()) { + marker_rename_error_dialog->set_text(TTR("Empty marker names are not allowed.")); + marker_rename_error_dialog->popup_centered(); + return; + } + + if (new_name != prev_name && animation->has_marker(new_name)) { + marker_rename_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), new_name)); + marker_rename_error_dialog->popup_centered(); + return; + } + + if (prev_name != new_name) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Rename Marker")); + undo_redo->add_do_method(animation.ptr(), "remove_marker", prev_name); + undo_redo->add_do_method(animation.ptr(), "add_marker", new_name, animation->get_marker_time(prev_name)); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", new_name, animation->get_marker_color(prev_name)); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", new_name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", prev_name, animation->get_marker_time(prev_name)); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", prev_name, animation->get_marker_color(prev_name)); + undo_redo->add_do_method(this, "_select_key", new_name, true); + undo_redo->add_undo_method(this, "_select_key", prev_name, true); + undo_redo->commit_action(); + select_single_attempt = StringName(); + } + marker_rename_confirm->hide(); +} + +void AnimationMarkerEdit::_marker_rename_new_name_changed(const String &p_text) { + marker_rename_confirm->get_ok_button()->set_disabled(p_text.is_empty()); +} + +AnimationMarkerEdit::AnimationMarkerEdit() { + play_position = memnew(Control); + play_position->set_mouse_filter(MOUSE_FILTER_PASS); + add_child(play_position); + play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationMarkerEdit::_play_position_draw)); + set_focus_mode(FOCUS_CLICK); + set_mouse_filter(MOUSE_FILTER_PASS); // Scroll has to work too for selection. + + menu = memnew(PopupMenu); + add_child(menu); + menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationMarkerEdit::_menu_selected)); + menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/rename_marker", TTR("Rename Marker"), Key::R), MENU_KEY_RENAME); + menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/delete_selection", TTR("Delete Markers (s)"), Key::KEY_DELETE), MENU_KEY_DELETE); + menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/toggle_marker_names", TTR("Show All Marker Names"), Key::M), MENU_KEY_TOGGLE_MARKER_NAMES); + + marker_insert_confirm = memnew(ConfirmationDialog); + marker_insert_confirm->set_title(TTR("Insert Marker")); + marker_insert_confirm->set_hide_on_ok(false); + marker_insert_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_confirmed)); + add_child(marker_insert_confirm); + VBoxContainer *marker_insert_vbox = memnew(VBoxContainer); + marker_insert_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT); + marker_insert_confirm->add_child(marker_insert_vbox); + marker_insert_new_name = memnew(LineEdit); + marker_insert_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_new_name_changed)); + marker_insert_confirm->register_text_enter(marker_insert_new_name); + marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Name"), marker_insert_new_name)); + marker_insert_color = memnew(ColorPickerButton); + marker_insert_color->set_edit_alpha(false); + marker_insert_color->get_popup()->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(marker_insert_color->get_picker())); + marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Color"), marker_insert_color)); + marker_insert_error_dialog = memnew(AcceptDialog); + marker_insert_error_dialog->set_ok_button_text(TTR("Close")); + marker_insert_error_dialog->set_title(TTR("Error!")); + marker_insert_confirm->add_child(marker_insert_error_dialog); + + marker_rename_confirm = memnew(ConfirmationDialog); + marker_rename_confirm->set_title(TTR("Rename Marker")); + marker_rename_confirm->set_hide_on_ok(false); + marker_rename_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_confirmed)); + add_child(marker_rename_confirm); + VBoxContainer *marker_rename_vbox = memnew(VBoxContainer); + marker_rename_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT); + marker_rename_confirm->add_child(marker_rename_vbox); + Label *marker_rename_new_name_label = memnew(Label); + marker_rename_new_name_label->set_text(TTR("Change Marker Name:")); + marker_rename_vbox->add_child(marker_rename_new_name_label); + marker_rename_new_name = memnew(LineEdit); + marker_rename_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_new_name_changed)); + marker_rename_confirm->register_text_enter(marker_rename_new_name); + marker_rename_vbox->add_child(marker_rename_new_name); + + marker_rename_error_dialog = memnew(AcceptDialog); + marker_rename_error_dialog->set_ok_button_text(TTR("Close")); + marker_rename_error_dialog->set_title(TTR("Error!")); + marker_rename_confirm->add_child(marker_rename_error_dialog); +} + +AnimationMarkerEdit::~AnimationMarkerEdit() { +} + +float AnimationMarkerKeyEdit::get_time() const { + return animation->get_marker_time(marker_name); +} + +void AnimationMarkerKeyEdit::_bind_methods() { + ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMarkerKeyEdit::_hide_script_from_inspector); + ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMarkerKeyEdit::_hide_metadata_from_inspector); + ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMarkerKeyEdit::_dont_undo_redo); + ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMarkerKeyEdit::_is_read_only); + ClassDB::bind_method(D_METHOD("_set_marker_name"), &AnimationMarkerKeyEdit::_set_marker_name); +} + +void AnimationMarkerKeyEdit::_set_marker_name(const StringName &p_name) { + marker_name = p_name; +} + +bool AnimationMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + if (p_name == "color") { + Color color = p_value; + Color prev_color = animation->get_marker_color(marker_name); + if (color != prev_color) { + undo_redo->create_action(TTR("Edit Marker Color"), UndoRedo::MERGE_ENDS); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, prev_color); + undo_redo->add_do_method(marker_edit, "queue_redraw"); + undo_redo->add_undo_method(marker_edit, "queue_redraw"); + undo_redo->commit_action(); + } + return true; + } + + return false; +} + +bool AnimationMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const { + if (p_name == "name") { + r_ret = marker_name; + return true; + } + + if (p_name == "color") { + r_ret = animation->get_marker_color(marker_name); + return true; + } + + return false; +} + +void AnimationMarkerKeyEdit::_get_property_list(List *p_list) const { + if (animation.is_null()) { + return; + } + + p_list->push_back(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR)); + p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA)); +} + +void AnimationMultiMarkerKeyEdit::_bind_methods() { + ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_script_from_inspector); + ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_metadata_from_inspector); + ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiMarkerKeyEdit::_dont_undo_redo); + ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiMarkerKeyEdit::_is_read_only); +} + +bool AnimationMultiMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + if (p_name == "color") { + Color color = p_value; + + undo_redo->create_action(TTR("Multi Edit Marker Color"), UndoRedo::MERGE_ENDS); + + for (const StringName &marker_name : marker_names) { + undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, animation->get_marker_color(marker_name)); + } + + undo_redo->add_do_method(marker_edit, "queue_redraw"); + undo_redo->add_undo_method(marker_edit, "queue_redraw"); + undo_redo->commit_action(); + + return true; + } + + return false; +} + +bool AnimationMultiMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const { + if (p_name == "color") { + r_ret = animation->get_marker_color(marker_names[0]); + return true; + } + + return false; +} + +void AnimationMultiMarkerKeyEdit::_get_property_list(List *p_list) const { + if (animation.is_null()) { + return; + } + + p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA)); +} + +// AnimationMarkerKeyEditEditorPlugin + +void AnimationMarkerKeyEditEditor::_time_edit_entered() { +} + +void AnimationMarkerKeyEditEditor::_time_edit_exited() { + real_t new_time = spinner->get_value(); + + if (use_fps) { + real_t fps = animation->get_step(); + if (fps > 0) { + fps = 1.0 / fps; + } + new_time /= fps; + } + + real_t prev_time = animation->get_marker_time(marker_name); + + if (Math::is_equal_approx(new_time, prev_time)) { + return; // No change. + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Change Marker Time"), UndoRedo::MERGE_ENDS); + + Color color = animation->get_marker_color(marker_name); + undo_redo->add_do_method(animation.ptr(), "add_marker", marker_name, new_time); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", marker_name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", marker_name, prev_time); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, color); + StringName existing_marker = animation->get_marker_at_time(new_time); + if (existing_marker) { + undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, animation->get_marker_time(existing_marker)); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker)); + } + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + AnimationTrackEditor *ate = ape->get_track_editor(); + if (ate) { + AnimationMarkerEdit *ame = ate->marker_edit; + undo_redo->add_do_method(ame, "queue_redraw"); + undo_redo->add_undo_method(ame, "queue_redraw"); + } + } + undo_redo->commit_action(); +} + +AnimationMarkerKeyEditEditor::AnimationMarkerKeyEditEditor(Ref p_animation, const StringName &p_name, bool p_use_fps) { + if (p_animation.is_null()) { + return; + } + + animation = p_animation; + use_fps = p_use_fps; + marker_name = p_name; + + set_label("Time"); + + spinner = memnew(EditorSpinSlider); + spinner->set_focus_mode(Control::FOCUS_CLICK); + spinner->set_min(0); + spinner->set_allow_greater(true); + spinner->set_allow_lesser(true); + + float time = animation->get_marker_time(marker_name); + + if (use_fps) { + spinner->set_step(FPS_DECIMAL); + real_t fps = animation->get_step(); + if (fps > 0) { + fps = 1.0 / fps; + } + spinner->set_value(time * fps); + } else { + spinner->set_step(SECOND_DECIMAL); + spinner->set_value(time); + spinner->set_max(animation->get_length()); + } + + add_child(spinner); + + spinner->connect("grabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED); + spinner->connect("ungrabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED); + spinner->connect("value_focus_entered", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED); + spinner->connect("value_focus_exited", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED); +} + +AnimationMarkerKeyEditEditor::~AnimationMarkerKeyEditEditor() { +} diff --git a/editor/animation_track_editor.h b/editor/animation_track_editor.h index 6b9140ddaa7..0da474afd40 100644 --- a/editor/animation_track_editor.h +++ b/editor/animation_track_editor.h @@ -41,9 +41,11 @@ #include "scene/gui/tree.h" #include "scene/resources/animation.h" +class AnimationMarkerEdit; class AnimationTrackEditor; class AnimationTrackEdit; class CheckBox; +class ColorPickerButton; class EditorSpinSlider; class HSlider; class OptionButton; @@ -52,6 +54,7 @@ class SceneTreeDialog; class SpinBox; class TextureRect; class ViewPanner; +class EditorValidationPanel; class AnimationTrackKeyEdit : public Object { GDCLASS(AnimationTrackKeyEdit, Object); @@ -128,6 +131,58 @@ class AnimationMultiTrackKeyEdit : public Object { void _get_property_list(List *p_list) const; }; +class AnimationMarkerKeyEdit : public Object { + GDCLASS(AnimationMarkerKeyEdit, Object); + +public: + bool animation_read_only = false; + + Ref animation; + StringName marker_name; + bool use_fps = false; + + AnimationMarkerEdit *marker_edit = nullptr; + + bool _hide_script_from_inspector() { return true; } + bool _hide_metadata_from_inspector() { return true; } + bool _dont_undo_redo() { return true; } + + bool _is_read_only() { return animation_read_only; } + + float get_time() const; + +protected: + static void _bind_methods(); + void _set_marker_name(const StringName &p_name); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List *p_list) const; +}; + +class AnimationMultiMarkerKeyEdit : public Object { + GDCLASS(AnimationMultiMarkerKeyEdit, Object); + +public: + bool animation_read_only = false; + + Ref animation; + Vector marker_names; + + AnimationMarkerEdit *marker_edit = nullptr; + + bool _hide_script_from_inspector() { return true; } + bool _hide_metadata_from_inspector() { return true; } + bool _dont_undo_redo() { return true; } + + bool _is_read_only() { return animation_read_only; } + +protected: + static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List *p_list) const; +}; + class AnimationTimelineEdit : public Range { GDCLASS(AnimationTimelineEdit, Range); @@ -218,6 +273,140 @@ class AnimationTimelineEdit : public Range { AnimationTimelineEdit(); }; +class AnimationMarkerEdit : public Control { + GDCLASS(AnimationMarkerEdit, Control); + friend class AnimationTimelineEdit; + + enum { + MENU_KEY_INSERT, + MENU_KEY_RENAME, + MENU_KEY_DELETE, + MENU_KEY_TOGGLE_MARKER_NAMES, + }; + + AnimationTimelineEdit *timeline = nullptr; + Control *play_position = nullptr; // Separate control used to draw so updates for only position changed are much faster. + float play_position_pos = 0.0f; + + HashSet selection; + + Ref animation; + bool read_only = false; + + Ref type_icon; + Ref selected_icon; + + PopupMenu *menu = nullptr; + + bool hovered = false; + StringName hovering_marker; + + void _zoom_changed(); + + Ref icon_cache; + + void _menu_selected(int p_index); + + void _play_position_draw(); + bool _try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable); + bool _is_ui_pos_in_current_section(const Point2 &p_pos); + + float insert_at_pos = 0.0f; + bool moving_selection_attempt = false; + bool moving_selection_effective = false; + float moving_selection_offset = 0.0f; + float moving_selection_pivot = 0.0f; + float moving_selection_mouse_begin_x = 0.0f; + float moving_selection_mouse_begin_y = 0.0f; + StringName select_single_attempt; + bool moving_selection = false; + void _move_selection_begin(); + void _move_selection(float p_offset); + void _move_selection_commit(); + void _move_selection_cancel(); + + void _clear_selection_for_anim(const Ref &p_anim); + void _select_key(const StringName &p_name, bool is_single = false); + void _deselect_key(const StringName &p_name); + + void _insert_marker(float p_ofs); + void _rename_marker(const StringName &p_name); + void _delete_selected_markers(); + + ConfirmationDialog *marker_insert_confirm = nullptr; + LineEdit *marker_insert_new_name = nullptr; + ColorPickerButton *marker_insert_color = nullptr; + AcceptDialog *marker_insert_error_dialog = nullptr; + float marker_insert_ofs = 0; + + ConfirmationDialog *marker_rename_confirm = nullptr; + LineEdit *marker_rename_new_name = nullptr; + StringName marker_rename_prev_name; + + AcceptDialog *marker_rename_error_dialog = nullptr; + + bool should_show_all_marker_names = false; + + ////////////// edit menu stuff + + void _marker_insert_confirmed(); + void _marker_insert_new_name_changed(const String &p_text); + void _marker_rename_confirmed(); + void _marker_rename_new_name_changed(const String &p_text); + + AnimationTrackEditor *editor = nullptr; + + HBoxContainer *_create_hbox_labeled_control(const String &p_text, Control *p_control) const; + + void _update_key_edit(); + void _clear_key_edit(); + + AnimationMarkerKeyEdit *key_edit = nullptr; + AnimationMultiMarkerKeyEdit *multi_key_edit = nullptr; + +protected: + static void _bind_methods(); + void _notification(int p_what); + + virtual void gui_input(const Ref &p_event) override; + +public: + virtual String get_tooltip(const Point2 &p_pos) const override; + + virtual int get_key_height() const; + virtual Rect2 get_key_rect(float p_pixels_sec) const; + virtual bool is_key_selectable_by_distance() const; + virtual void draw_key(const StringName &p_name, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right); + virtual void draw_bg(int p_clip_left, int p_clip_right); + virtual void draw_fg(int p_clip_left, int p_clip_right); + + Ref get_animation() const; + AnimationTimelineEdit *get_timeline() const { return timeline; } + AnimationTrackEditor *get_editor() const { return editor; } + bool is_selection_active() const { return !selection.is_empty(); } + bool is_moving_selection() const { return moving_selection; } + float get_moving_selection_offset() const { return moving_selection_offset; } + void set_animation(const Ref &p_animation, bool p_read_only); + virtual Size2 get_minimum_size() const override; + + void set_timeline(AnimationTimelineEdit *p_timeline); + void set_editor(AnimationTrackEditor *p_editor); + + void set_play_position(float p_pos); + void update_play_position(); + + void set_use_fps(bool p_use_fps); + + PackedStringArray get_selected_section() const; + bool is_marker_selected(const StringName &p_marker) const; + + // For use by AnimationTrackEditor. + void _clear_selection(bool p_update); + + AnimationMarkerEdit(); + ~AnimationMarkerEdit(); +}; + class AnimationTrackEdit : public Control { GDCLASS(AnimationTrackEdit, Control); friend class AnimationTimelineEdit; @@ -367,6 +556,7 @@ class AnimationTrackEditGroup : public Control { NodePath node; Node *root = nullptr; AnimationTimelineEdit *timeline = nullptr; + AnimationTrackEditor *editor = nullptr; void _zoom_changed(); @@ -380,6 +570,7 @@ class AnimationTrackEditGroup : public Control { virtual Size2 get_minimum_size() const override; void set_timeline(AnimationTimelineEdit *p_timeline); void set_root(Node *p_root); + void set_editor(AnimationTrackEditor *p_editor); AnimationTrackEditGroup(); }; @@ -388,6 +579,7 @@ class AnimationTrackEditor : public VBoxContainer { GDCLASS(AnimationTrackEditor, VBoxContainer); friend class AnimationTimelineEdit; friend class AnimationBezierTrackEdit; + friend class AnimationMarkerKeyEditEditor; Ref animation; bool read_only = false; @@ -405,6 +597,7 @@ class AnimationTrackEditor : public VBoxContainer { Label *info_message = nullptr; AnimationTimelineEdit *timeline = nullptr; + AnimationMarkerEdit *marker_edit = nullptr; HSlider *zoom = nullptr; EditorSpinSlider *step = nullptr; TextureRect *zoom_icon = nullptr; @@ -743,6 +936,10 @@ class AnimationTrackEditor : public VBoxContainer { float get_moving_selection_offset() const; float snap_time(float p_value, bool p_relative = false); bool is_grouping_tracks(); + PackedStringArray get_selected_section() const; + bool is_marker_selected(const StringName &p_marker) const; + bool is_marker_moving_selection() const; + float get_marker_moving_selection_offset() const; /** If `p_from_mouse_event` is `true`, handle Shift key presses for precise snapping. */ void goto_prev_step(bool p_from_mouse_event); @@ -781,4 +978,23 @@ class AnimationTrackKeyEditEditor : public EditorProperty { ~AnimationTrackKeyEditEditor(); }; +// AnimationMarkerKeyEditEditorPlugin + +class AnimationMarkerKeyEditEditor : public EditorProperty { + GDCLASS(AnimationMarkerKeyEditEditor, EditorProperty); + + Ref animation; + StringName marker_name; + bool use_fps = false; + + EditorSpinSlider *spinner = nullptr; + + void _time_edit_entered(); + void _time_edit_exited(); + +public: + AnimationMarkerKeyEditEditor(Ref p_animation, const StringName &p_name, bool p_use_fps); + ~AnimationMarkerKeyEditEditor(); +}; + #endif // ANIMATION_TRACK_EDITOR_H diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 665255b9b24..3bae9ae984e 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -7722,6 +7722,7 @@ EditorNode::EditorNode() { add_editor_plugin(memnew(AnimationPlayerEditorPlugin)); add_editor_plugin(memnew(AnimationTrackKeyEditEditorPlugin)); + add_editor_plugin(memnew(AnimationMarkerKeyEditEditorPlugin)); add_editor_plugin(memnew(CanvasItemEditorPlugin)); add_editor_plugin(memnew(Node3DEditorPlugin)); add_editor_plugin(memnew(ScriptEditorPlugin)); diff --git a/editor/icons/Marker.svg b/editor/icons/Marker.svg new file mode 100644 index 00000000000..ff91a4a9472 --- /dev/null +++ b/editor/icons/Marker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/icons/MarkerSelected.svg b/editor/icons/MarkerSelected.svg new file mode 100644 index 00000000000..c581a3a651b --- /dev/null +++ b/editor/icons/MarkerSelected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/plugins/animation_blend_tree_editor_plugin.cpp b/editor/plugins/animation_blend_tree_editor_plugin.cpp index a28fe016667..9e282cb3fa1 100644 --- a/editor/plugins/animation_blend_tree_editor_plugin.cpp +++ b/editor/plugins/animation_blend_tree_editor_plugin.cpp @@ -36,6 +36,7 @@ #include "core/os/keyboard.h" #include "editor/editor_inspector.h" #include "editor/editor_node.h" +#include "editor/editor_properties.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" @@ -45,6 +46,7 @@ #include "scene/animation/animation_player.h" #include "scene/gui/check_box.h" #include "scene/gui/menu_button.h" +#include "scene/gui/option_button.h" #include "scene/gui/panel.h" #include "scene/gui/progress_bar.h" #include "scene/gui/separator.h" @@ -1262,4 +1264,168 @@ AnimationNodeBlendTreeEditor::AnimationNodeBlendTreeEditor() { open_file->set_title(TTR("Open Animation Node")); open_file->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE); open_file->connect("file_selected", callable_mp(this, &AnimationNodeBlendTreeEditor::_file_opened)); + + animation_node_inspector_plugin = Ref(memnew(EditorInspectorPluginAnimationNodeAnimation)); + EditorInspector::add_inspector_plugin(animation_node_inspector_plugin); +} + +AnimationNodeBlendTreeEditor::~AnimationNodeBlendTreeEditor() { +} + +// EditorPluginAnimationNodeAnimation + +void AnimationNodeAnimationEditor::_open_set_custom_timeline_from_marker_dialog() { + AnimationTree *tree = AnimationTreeEditor::get_singleton()->get_animation_tree(); + StringName anim_name = animation_node_animation->get_animation(); + PackedStringArray markers = tree->has_animation(anim_name) ? tree->get_animation(anim_name)->get_marker_names() : PackedStringArray(); + + dialog->select_start->clear(); + dialog->select_start->add_icon_item(get_editor_theme_icon(SNAME("PlayStart")), TTR("Start of Animation")); + dialog->select_start->add_separator(); + dialog->select_end->clear(); + dialog->select_end->add_icon_item(get_editor_theme_icon(SNAME("PlayStartBackwards")), TTR("End of Animation")); + dialog->select_end->add_separator(); + + for (const String &marker : markers) { + dialog->select_start->add_item(marker); + dialog->select_end->add_item(marker); + } + + // Because the default selections are always valid, and marker times won't change during the dialog, we can ensure that the user can only select valid markers. + // This invariant is maintained by _validate_markers. + dialog->select_start->select(0); + dialog->select_end->select(0); + + dialog->popup_centered(Size2(200, 0) * EDSCALE); +} + +void AnimationNodeAnimationEditor::_validate_markers(int p_id) { + // Note: p_id is ignored. It is included because OptionButton's item_changed signal always passes it. + int start_id = dialog->select_start->get_selected_id(); + int end_id = dialog->select_end->get_selected_id(); + + StringName anim_name = animation_node_animation->get_animation(); + Ref animation = AnimationTreeEditor::get_singleton()->get_animation_tree()->get_animation(anim_name); + ERR_FAIL_COND(animation.is_null()); + + double start_time = start_id < 2 ? 0 : animation->get_marker_time(dialog->select_start->get_item_text(start_id)); + double end_time = end_id < 2 ? animation->get_length() : animation->get_marker_time(dialog->select_end->get_item_text(end_id)); + + // p_start and p_end have the same item count. + for (int i = 2; i < dialog->select_start->get_item_count(); i++) { + String start_marker = dialog->select_start->get_item_text(i); + String end_marker = dialog->select_end->get_item_text(i); + dialog->select_start->set_item_disabled(i, end_id >= 2 && (i == end_id || animation->get_marker_time(start_marker) > end_time)); + dialog->select_end->set_item_disabled(i, start_id >= 2 && (i == start_id || start_time > animation->get_marker_time(end_marker))); + } +} + +void AnimationNodeAnimationEditor::_confirm_set_custom_timeline_from_marker_dialog() { + int start_id = dialog->select_start->get_selected_id(); + int end_id = dialog->select_end->get_selected_id(); + + Ref animation = AnimationTreeEditor::get_singleton()->get_animation_tree()->get_animation(animation_node_animation->get_animation()); + ERR_FAIL_COND(animation.is_null()); + double start_time = start_id < 2 ? 0 : animation->get_marker_time(dialog->select_start->get_item_text(start_id)); + double end_time = end_id < 2 ? animation->get_length() : animation->get_marker_time(dialog->select_end->get_item_text(end_id)); + double length = end_time - start_time; + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Set Custom Timeline from Marker")); + undo_redo->add_do_method(*animation_node_animation, "set_start_offset", start_time); + undo_redo->add_undo_method(*animation_node_animation, "set_start_offset", animation_node_animation->get_start_offset()); + undo_redo->add_do_method(*animation_node_animation, "set_stretch_time_scale", false); + undo_redo->add_undo_method(*animation_node_animation, "set_stretch_time_scale", animation_node_animation->is_stretching_time_scale()); + undo_redo->add_do_method(*animation_node_animation, "set_timeline_length", length); + undo_redo->add_undo_method(*animation_node_animation, "set_timeline_length", animation_node_animation->get_timeline_length()); + undo_redo->add_do_method(*animation_node_animation, "notify_property_list_changed"); + undo_redo->add_undo_method(*animation_node_animation, "notify_property_list_changed"); + undo_redo->commit_action(); +} + +AnimationNodeAnimationEditor::AnimationNodeAnimationEditor(Ref p_animation_node_animation) { + animation_node_animation = p_animation_node_animation; + + dialog = memnew(AnimationNodeAnimationEditorDialog); + add_child(dialog); + dialog->set_hide_on_ok(false); + dialog->select_start->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeAnimationEditor::_validate_markers)); + dialog->select_end->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeAnimationEditor::_validate_markers)); + dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationNodeAnimationEditor::_confirm_set_custom_timeline_from_marker_dialog)); + + Control *top_spacer = memnew(Control); + add_child(top_spacer); + top_spacer->set_custom_minimum_size(Size2(0, 2) * EDSCALE); + + button = memnew(Button); + add_child(button); + button->set_text(TTR("Set Custom Timeline from Marker")); + button->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + button->connect(SceneStringName(pressed), callable_mp(this, &AnimationNodeAnimationEditor::_open_set_custom_timeline_from_marker_dialog)); + + Control *bottom_spacer = memnew(Control); + add_child(bottom_spacer); + bottom_spacer->set_custom_minimum_size(Size2(0, 2) * EDSCALE); +} + +AnimationNodeAnimationEditor::~AnimationNodeAnimationEditor() { +} + +void AnimationNodeAnimationEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + button->set_theme_type_variation(SNAME("InspectorActionButton")); + button->set_icon(get_editor_theme_icon(SNAME("Edit"))); + } break; + } +} + +bool EditorInspectorPluginAnimationNodeAnimation::can_handle(Object *p_object) { + Ref ana(Object::cast_to(p_object)); + return ana.is_valid() && ana->is_using_custom_timeline(); +} + +bool EditorInspectorPluginAnimationNodeAnimation::parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField p_usage, const bool p_wide) { + Ref ana(Object::cast_to(p_object)); + ERR_FAIL_COND_V(ana.is_null(), false); + + if (p_path == "timeline_length") { + add_custom_control(memnew(AnimationNodeAnimationEditor(ana))); + } + + return false; +} + +AnimationNodeAnimationEditorDialog::AnimationNodeAnimationEditorDialog() { + set_title(TTR("Select Markers...")); + VBoxContainer *vbox = memnew(VBoxContainer); + add_child(vbox); + vbox->set_offsets_preset(Control::PRESET_FULL_RECT); + + HBoxContainer *container_start = memnew(HBoxContainer); + vbox->add_child(container_start); + Label *label_start = memnew(Label); + container_start->add_child(label_start); + label_start->set_h_size_flags(Control::SIZE_EXPAND_FILL); + label_start->set_stretch_ratio(1); + label_start->set_text(TTR("Start Marker")); + select_start = memnew(OptionButton); + container_start->add_child(select_start); + select_start->set_h_size_flags(Control::SIZE_EXPAND_FILL); + select_start->set_stretch_ratio(2); + + HBoxContainer *container_end = memnew(HBoxContainer); + vbox->add_child(container_end); + Label *label_end = memnew(Label); + container_end->add_child(label_end); + label_end->set_h_size_flags(Control::SIZE_EXPAND_FILL); + label_end->set_stretch_ratio(1); + label_end->set_text(TTR("End Marker")); + select_end = memnew(OptionButton); + container_end->add_child(select_end); + select_end->set_h_size_flags(Control::SIZE_EXPAND_FILL); + select_end->set_stretch_ratio(2); +} + +AnimationNodeAnimationEditorDialog::~AnimationNodeAnimationEditorDialog() { } diff --git a/editor/plugins/animation_blend_tree_editor_plugin.h b/editor/plugins/animation_blend_tree_editor_plugin.h index ee6f087e071..9e7793977ba 100644 --- a/editor/plugins/animation_blend_tree_editor_plugin.h +++ b/editor/plugins/animation_blend_tree_editor_plugin.h @@ -32,9 +32,11 @@ #define ANIMATION_BLEND_TREE_EDITOR_PLUGIN_H #include "core/object/script_language.h" +#include "editor/editor_inspector.h" #include "editor/plugins/animation_tree_editor_plugin.h" #include "scene/animation/animation_blend_tree.h" #include "scene/gui/button.h" +#include "scene/gui/dialogs.h" #include "scene/gui/graph_edit.h" #include "scene/gui/panel_container.h" #include "scene/gui/popup.h" @@ -47,6 +49,7 @@ class EditorFileDialog; class EditorProperty; class MenuButton; class PanelContainer; +class EditorInspectorPluginAnimationNodeAnimation; class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin { GDCLASS(AnimationNodeBlendTreeEditor, AnimationTreeNodeEditorPlugin); @@ -147,6 +150,8 @@ class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin { MENU_LOAD_FILE_CONFIRM = 1002 }; + Ref animation_node_inspector_plugin; + protected: void _notification(int p_what); static void _bind_methods(); @@ -165,6 +170,48 @@ class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin { void update_graph(); AnimationNodeBlendTreeEditor(); + ~AnimationNodeBlendTreeEditor(); +}; + +// EditorPluginAnimationNodeAnimation + +class EditorInspectorPluginAnimationNodeAnimation : public EditorInspectorPlugin { + GDCLASS(EditorInspectorPluginAnimationNodeAnimation, EditorInspectorPlugin); + +public: + virtual bool can_handle(Object *p_object) override; + virtual bool parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField p_usage, const bool p_wide) override; +}; + +class AnimationNodeAnimationEditorDialog : public ConfirmationDialog { + GDCLASS(AnimationNodeAnimationEditorDialog, ConfirmationDialog); + + friend class AnimationNodeAnimationEditor; + + OptionButton *select_start = nullptr; + OptionButton *select_end = nullptr; + +public: + AnimationNodeAnimationEditorDialog(); + ~AnimationNodeAnimationEditorDialog(); +}; + +class AnimationNodeAnimationEditor : public VBoxContainer { + GDCLASS(AnimationNodeAnimationEditor, VBoxContainer); + + Ref animation_node_animation; + Button *button = nullptr; + AnimationNodeAnimationEditorDialog *dialog = nullptr; + void _open_set_custom_timeline_from_marker_dialog(); + void _validate_markers(int p_id); + void _confirm_set_custom_timeline_from_marker_dialog(); + +public: + AnimationNodeAnimationEditor(Ref p_animation_node_animation); + ~AnimationNodeAnimationEditor(); + +protected: + void _notification(int p_what); }; #endif // ANIMATION_BLEND_TREE_EDITOR_PLUGIN_H diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp index 5cb558abbee..e6afc85e9ee 100644 --- a/editor/plugins/animation_player_editor_plugin.cpp +++ b/editor/plugins/animation_player_editor_plugin.cpp @@ -41,6 +41,7 @@ #include "editor/editor_undo_redo_manager.h" #include "editor/gui/editor_bottom_panel.h" #include "editor/gui/editor_file_dialog.h" +#include "editor/gui/editor_validation_panel.h" #include "editor/inspector_dock.h" #include "editor/plugins/canvas_item_editor_plugin.h" // For onion skinning. #include "editor/plugins/node_3d_editor_plugin.h" // For onion skinning. @@ -295,7 +296,14 @@ void AnimationPlayerEditor::_play_pressed() { player->stop(); //so it won't blend with itself } ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing."); - player->play(current); + PackedStringArray markers = track_editor->get_selected_section(); + if (markers.size() == 2) { + StringName start_marker = markers[0]; + StringName end_marker = markers[1]; + player->play_section_with_markers(current, start_marker, end_marker); + } else { + player->play(current); + } } //unstop @@ -312,7 +320,14 @@ void AnimationPlayerEditor::_play_from_pressed() { } ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing."); player->seek_internal(time, true, true, true); - player->play(current); + PackedStringArray markers = track_editor->get_selected_section(); + if (markers.size() == 2) { + StringName start_marker = markers[0]; + StringName end_marker = markers[1]; + player->play_section_with_markers(current, start_marker, end_marker); + } else { + player->play(current); + } } //unstop @@ -333,7 +348,14 @@ void AnimationPlayerEditor::_play_bw_pressed() { player->stop(); //so it won't blend with itself } ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing."); - player->play_backwards(current); + PackedStringArray markers = track_editor->get_selected_section(); + if (markers.size() == 2) { + StringName start_marker = markers[0]; + StringName end_marker = markers[1]; + player->play_section_with_markers_backwards(current, start_marker, end_marker); + } else { + player->play_backwards(current); + } } //unstop @@ -350,7 +372,14 @@ void AnimationPlayerEditor::_play_bw_from_pressed() { } ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing."); player->seek_internal(time, true, true, true); - player->play_backwards(current); + PackedStringArray markers = track_editor->get_selected_section(); + if (markers.size() == 2) { + StringName start_marker = markers[0]; + StringName end_marker = markers[1]; + player->play_section_with_markers_backwards(current, start_marker, end_marker); + } else { + player->play_backwards(current); + } } //unstop @@ -2397,3 +2426,24 @@ AnimationTrackKeyEditEditorPlugin::AnimationTrackKeyEditEditorPlugin() { bool AnimationTrackKeyEditEditorPlugin::handles(Object *p_object) const { return p_object->is_class("AnimationTrackKeyEdit"); } + +bool EditorInspectorPluginAnimationMarkerKeyEdit::can_handle(Object *p_object) { + return Object::cast_to(p_object) != nullptr; +} + +void EditorInspectorPluginAnimationMarkerKeyEdit::parse_begin(Object *p_object) { + AnimationMarkerKeyEdit *amk = Object::cast_to(p_object); + ERR_FAIL_NULL(amk); + + amk_editor = memnew(AnimationMarkerKeyEditEditor(amk->animation, amk->marker_name, amk->use_fps)); + add_custom_control(amk_editor); +} + +AnimationMarkerKeyEditEditorPlugin::AnimationMarkerKeyEditEditorPlugin() { + amk_plugin = memnew(EditorInspectorPluginAnimationMarkerKeyEdit); + EditorInspector::add_inspector_plugin(amk_plugin); +} + +bool AnimationMarkerKeyEditEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("AnimationMarkerKeyEdit"); +} diff --git a/editor/plugins/animation_player_editor_plugin.h b/editor/plugins/animation_player_editor_plugin.h index e4ca6c17c39..349ed7b5cd4 100644 --- a/editor/plugins/animation_player_editor_plugin.h +++ b/editor/plugins/animation_player_editor_plugin.h @@ -338,4 +338,30 @@ class AnimationTrackKeyEditEditorPlugin : public EditorPlugin { AnimationTrackKeyEditEditorPlugin(); }; +// AnimationMarkerKeyEditEditorPlugin + +class EditorInspectorPluginAnimationMarkerKeyEdit : public EditorInspectorPlugin { + GDCLASS(EditorInspectorPluginAnimationMarkerKeyEdit, EditorInspectorPlugin); + + AnimationMarkerKeyEditEditor *amk_editor = nullptr; + +public: + virtual bool can_handle(Object *p_object) override; + virtual void parse_begin(Object *p_object) override; +}; + +class AnimationMarkerKeyEditEditorPlugin : public EditorPlugin { + GDCLASS(AnimationMarkerKeyEditEditorPlugin, EditorPlugin); + + EditorInspectorPluginAnimationMarkerKeyEdit *amk_plugin = nullptr; + +public: + bool has_main_screen() const override { return false; } + virtual bool handles(Object *p_object) const override; + + virtual String get_name() const override { return "AnimationMarkerKeyEdit"; } + + AnimationMarkerKeyEditEditorPlugin(); +}; + #endif // ANIMATION_PLAYER_EDITOR_PLUGIN_H diff --git a/scene/animation/animation_blend_tree.cpp b/scene/animation/animation_blend_tree.cpp index a96417738f8..a2aef60417c 100644 --- a/scene/animation/animation_blend_tree.cpp +++ b/scene/animation/animation_blend_tree.cpp @@ -245,6 +245,8 @@ AnimationNode::NodeTimeInfo AnimationNodeAnimation::_process(const AnimationMixe if (!p_test_only) { AnimationMixer::PlaybackInfo pi = p_playback_info; + pi.start = 0.0; + pi.end = cur_len; if (play_mode == PLAY_MODE_FORWARD) { pi.time = cur_playback_time; pi.delta = cur_delta; diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index 664302d45ea..eb8bc8c3824 100644 --- a/scene/animation/animation_mixer.cpp +++ b/scene/animation/animation_mixer.cpp @@ -1117,6 +1117,8 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { Ref a = ai.animation_data.animation; double time = ai.playback_info.time; double delta = ai.playback_info.delta; + double start = ai.playback_info.start; + double end = ai.playback_info.end; bool seeked = ai.playback_info.seeked; Animation::LoopedFlag looped_flag = ai.playback_info.looped_flag; bool is_external_seeking = ai.playback_info.is_external_seeking; @@ -1168,32 +1170,32 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (track->root_motion && calc_root) { double prev_time = time - delta; if (!backward) { - if (Animation::is_less_approx(prev_time, 0)) { + if (Animation::is_less_approx(prev_time, start)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = 0; + prev_time = start; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; } } } else { - if (Animation::is_greater_approx(prev_time, (double)a_length)) { + if (Animation::is_greater_approx(prev_time, end)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = (double)a_length; + prev_time = end; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; @@ -1208,10 +1210,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } loc[0] = post_process_key_value(a, i, loc[0], t->object_id, t->bone_idx); - a->try_position_track_interpolate(i, (double)a_length, &loc[1]); + a->try_position_track_interpolate(i, end, &loc[1]); loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx); root_motion_cache.loc += (loc[1] - loc[0]) * blend; - prev_time = 0; + prev_time = start; } } else { if (Animation::is_less_approx(prev_time, time)) { @@ -1220,10 +1222,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } loc[0] = post_process_key_value(a, i, loc[0], t->object_id, t->bone_idx); - a->try_position_track_interpolate(i, 0, &loc[1]); + a->try_position_track_interpolate(i, start, &loc[1]); loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx); root_motion_cache.loc += (loc[1] - loc[0]) * blend; - prev_time = (double)a_length; + prev_time = end; } } Error err = a->try_position_track_interpolate(i, prev_time, &loc[0]); @@ -1234,7 +1236,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_position_track_interpolate(i, time, &loc[1]); loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx); root_motion_cache.loc += (loc[1] - loc[0]) * blend; - prev_time = !backward ? 0 : (double)a_length; + prev_time = !backward ? start : end; } { Vector3 loc; @@ -1256,32 +1258,32 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (track->root_motion && calc_root) { double prev_time = time - delta; if (!backward) { - if (Animation::is_less_approx(prev_time, 0)) { + if (Animation::is_less_approx(prev_time, start)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = 0; + prev_time = start; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; } } } else { - if (Animation::is_greater_approx(prev_time, (double)a_length)) { + if (Animation::is_greater_approx(prev_time, end)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = (double)a_length; + prev_time = end; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; @@ -1296,10 +1298,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } rot[0] = post_process_key_value(a, i, rot[0], t->object_id, t->bone_idx); - a->try_rotation_track_interpolate(i, (double)a_length, &rot[1]); + a->try_rotation_track_interpolate(i, end, &rot[1]); rot[1] = post_process_key_value(a, i, rot[1], t->object_id, t->bone_idx); root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized(); - prev_time = 0; + prev_time = start; } } else { if (Animation::is_less_approx(prev_time, time)) { @@ -1308,9 +1310,9 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } rot[0] = post_process_key_value(a, i, rot[0], t->object_id, t->bone_idx); - a->try_rotation_track_interpolate(i, 0, &rot[1]); + a->try_rotation_track_interpolate(i, start, &rot[1]); root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized(); - prev_time = (double)a_length; + prev_time = end; } } Error err = a->try_rotation_track_interpolate(i, prev_time, &rot[0]); @@ -1321,7 +1323,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_rotation_track_interpolate(i, time, &rot[1]); rot[1] = post_process_key_value(a, i, rot[1], t->object_id, t->bone_idx); root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized(); - prev_time = !backward ? 0 : (double)a_length; + prev_time = !backward ? start : end; } { Quaternion rot; @@ -1343,32 +1345,32 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (track->root_motion && calc_root) { double prev_time = time - delta; if (!backward) { - if (Animation::is_less_approx(prev_time, 0)) { + if (Animation::is_less_approx(prev_time, start)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = 0; + prev_time = start; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; } } } else { - if (Animation::is_greater_approx(prev_time, (double)a_length)) { + if (Animation::is_greater_approx(prev_time, end)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = (double)a_length; + prev_time = end; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; @@ -1383,10 +1385,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } scale[0] = post_process_key_value(a, i, scale[0], t->object_id, t->bone_idx); - a->try_scale_track_interpolate(i, (double)a_length, &scale[1]); + a->try_scale_track_interpolate(i, end, &scale[1]); root_motion_cache.scale += (scale[1] - scale[0]) * blend; scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx); - prev_time = 0; + prev_time = start; } } else { if (Animation::is_less_approx(prev_time, time)) { @@ -1395,10 +1397,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } scale[0] = post_process_key_value(a, i, scale[0], t->object_id, t->bone_idx); - a->try_scale_track_interpolate(i, 0, &scale[1]); + a->try_scale_track_interpolate(i, start, &scale[1]); scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx); root_motion_cache.scale += (scale[1] - scale[0]) * blend; - prev_time = (double)a_length; + prev_time = end; } } Error err = a->try_scale_track_interpolate(i, prev_time, &scale[0]); @@ -1409,7 +1411,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_scale_track_interpolate(i, time, &scale[1]); scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx); root_motion_cache.scale += (scale[1] - scale[0]) * blend; - prev_time = !backward ? 0 : (double)a_length; + prev_time = !backward ? start : end; } { Vector3 scale; @@ -1671,6 +1673,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (!player2) { continue; } + // TODO: Make it possible to embed section info in animation track keys. if (seeked) { // Seek. int idx = a->track_find_key(i, time, Animation::FIND_MODE_NEAREST, true); @@ -1683,19 +1686,19 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } Ref anim = player2->get_animation(anim_name); - double at_anim_pos = 0.0; + double at_anim_pos = start; switch (anim->get_loop_mode()) { case Animation::LOOP_NONE: { - if (!is_external_seeking && ((!backward && Animation::is_greater_or_equal_approx(time, pos + (double)anim->get_length())) || (backward && Animation::is_less_or_equal_approx(time, pos)))) { + if (!is_external_seeking && ((!backward && Animation::is_greater_or_equal_approx(time, pos + end)) || (backward && Animation::is_less_or_equal_approx(time, pos + start)))) { continue; // Do nothing if current time is outside of length when started. } - at_anim_pos = MIN((double)anim->get_length(), time - pos); // Seek to end. + at_anim_pos = MIN(end, time - pos); // Seek to end. } break; case Animation::LOOP_LINEAR: { - at_anim_pos = Math::fposmod(time - pos, (double)anim->get_length()); // Seek to loop. + at_anim_pos = Math::fposmod(time - pos - start, end - start) + start; // Seek to loop. } break; case Animation::LOOP_PINGPONG: { - at_anim_pos = Math::pingpong(time - pos, (double)a_length); + at_anim_pos = Math::pingpong(time - pos - start, end - start) + start; } break; default: break; @@ -2092,6 +2095,8 @@ Ref AnimationMixer::make_backup() { PlaybackInfo pi; pi.time = 0; pi.delta = 0; + pi.start = 0; + pi.end = reset_anim->get_length(); pi.seeked = true; pi.weight = 1.0; make_animation_instance(SceneStringName(RESET), pi); diff --git a/scene/animation/animation_mixer.h b/scene/animation/animation_mixer.h index 5482197fbdf..27c9a00a9c5 100644 --- a/scene/animation/animation_mixer.h +++ b/scene/animation/animation_mixer.h @@ -85,6 +85,8 @@ class AnimationMixer : public Node { struct PlaybackInfo { double time = 0.0; double delta = 0.0; + double start = 0.0; + double end = 0.0; bool seeked = false; bool is_external_seeking = false; Animation::LoopedFlag looped_flag = Animation::LOOPED_FLAG_NONE; diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp index a4aa383a9de..bc951e4e149 100644 --- a/scene/animation/animation_player.cpp +++ b/scene/animation/animation_player.cpp @@ -164,39 +164,41 @@ void AnimationPlayer::_process_playback_data(PlaybackData &cd, double p_delta, f double delta = p_started ? 0 : p_delta * speed; double next_pos = cd.pos + delta; - double len = cd.from->animation->get_length(); + double start = get_section_start_time(); + double end = get_section_end_time(); + Animation::LoopedFlag looped_flag = Animation::LOOPED_FLAG_NONE; switch (cd.from->animation->get_loop_mode()) { case Animation::LOOP_NONE: { - if (Animation::is_less_approx(next_pos, 0)) { - next_pos = 0; - } else if (Animation::is_greater_approx(next_pos, len)) { - next_pos = len; + if (Animation::is_less_approx(next_pos, start)) { + next_pos = start; + } else if (Animation::is_greater_approx(next_pos, end)) { + next_pos = end; } delta = next_pos - cd.pos; // Fix delta (after determination of backwards because negative zero is lost here). } break; case Animation::LOOP_LINEAR: { - if (Animation::is_less_approx(next_pos, 0) && Animation::is_greater_or_equal_approx(cd.pos, 0)) { + if (Animation::is_less_approx(next_pos, start) && Animation::is_greater_or_equal_approx(cd.pos, start)) { looped_flag = Animation::LOOPED_FLAG_START; } - if (Animation::is_greater_approx(next_pos, len) && Animation::is_less_or_equal_approx(cd.pos, len)) { + if (Animation::is_greater_approx(next_pos, end) && Animation::is_less_or_equal_approx(cd.pos, end)) { looped_flag = Animation::LOOPED_FLAG_END; } - next_pos = Math::fposmod(next_pos, (double)len); + next_pos = Math::fposmod(next_pos - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - if (Animation::is_less_approx(next_pos, 0) && Animation::is_greater_or_equal_approx(cd.pos, 0)) { + if (Animation::is_less_approx(next_pos, start) && Animation::is_greater_or_equal_approx(cd.pos, start)) { cd.speed_scale *= -1.0; looped_flag = Animation::LOOPED_FLAG_START; } - if (Animation::is_greater_approx(next_pos, len) && Animation::is_less_or_equal_approx(cd.pos, len)) { + if (Animation::is_greater_approx(next_pos, end) && Animation::is_less_or_equal_approx(cd.pos, end)) { cd.speed_scale *= -1.0; looped_flag = Animation::LOOPED_FLAG_END; } - next_pos = Math::pingpong(next_pos, (double)len); + next_pos = Math::pingpong(next_pos - start, end - start) + start; } break; default: @@ -208,18 +210,18 @@ void AnimationPlayer::_process_playback_data(PlaybackData &cd, double p_delta, f // End detection. if (p_is_current) { if (cd.from->animation->get_loop_mode() == Animation::LOOP_NONE) { - if (!backwards && Animation::is_less_or_equal_approx(prev_pos, len) && Math::is_equal_approx(next_pos, len)) { + if (!backwards && Animation::is_less_or_equal_approx(prev_pos, end) && Math::is_equal_approx(next_pos, end)) { // Playback finished. - next_pos = len; // Snap to the edge. + next_pos = end; // Snap to the edge. end_reached = true; - end_notify = Animation::is_less_approx(prev_pos, len); // Notify only if not already at the end. + end_notify = Animation::is_less_approx(prev_pos, end); // Notify only if not already at the end. p_blend = 1.0; } - if (backwards && Animation::is_greater_or_equal_approx(prev_pos, 0) && Math::is_equal_approx(next_pos, 0)) { + if (backwards && Animation::is_greater_or_equal_approx(prev_pos, start) && Math::is_equal_approx(next_pos, start)) { // Playback finished. - next_pos = 0; // Snap to the edge. + next_pos = start; // Snap to the edge. end_reached = true; - end_notify = Animation::is_greater_approx(prev_pos, 0); // Notify only if not already at the beginning. + end_notify = Animation::is_greater_approx(prev_pos, start); // Notify only if not already at the beginning. p_blend = 1.0; } } @@ -231,10 +233,14 @@ void AnimationPlayer::_process_playback_data(PlaybackData &cd, double p_delta, f if (p_started) { pi.time = prev_pos; pi.delta = 0; + pi.start = start; + pi.end = end; pi.seeked = true; } else { pi.time = next_pos; pi.delta = delta; + pi.start = start; + pi.end = end; pi.seeked = p_seeked; } if (Math::is_zero_approx(pi.delta) && backwards) { @@ -378,6 +384,14 @@ void AnimationPlayer::play_backwards(const StringName &p_name, double p_custom_b play(p_name, p_custom_blend, -1, true); } +void AnimationPlayer::play_section_with_markers_backwards(const StringName &p_name, const StringName &p_start_marker, const StringName &p_end_marker, double p_custom_blend) { + play_section_with_markers(p_name, p_start_marker, p_end_marker, p_custom_blend, -1, true); +} + +void AnimationPlayer::play_section_backwards(const StringName &p_name, double p_start_time, double p_end_time, double p_custom_blend) { + play_section(p_name, p_start_time, p_end_time, -1, true); +} + void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { if (auto_capture) { play_with_capture(p_name, auto_capture_duration, p_custom_blend, p_custom_scale, p_from_end, auto_capture_transition_type, auto_capture_ease_type); @@ -387,6 +401,10 @@ void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, floa } void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { + play_section_with_markers(p_name, StringName(), StringName(), p_custom_blend, p_custom_scale, p_from_end); +} + +void AnimationPlayer::play_section_with_markers(const StringName &p_name, const StringName &p_start_marker, const StringName &p_end_marker, double p_custom_blend, float p_custom_scale, bool p_from_end) { StringName name = p_name; if (name == StringName()) { @@ -395,6 +413,38 @@ void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, flo ERR_FAIL_COND_MSG(!animation_set.has(name), vformat("Animation not found: %s.", name)); + Ref animation = animation_set[name].animation; + + ERR_FAIL_COND_MSG(p_start_marker == p_end_marker && p_start_marker, vformat("Start marker and end marker cannot be the same marker: %s.", p_start_marker)); + ERR_FAIL_COND_MSG(p_start_marker && !animation->has_marker(p_start_marker), vformat("Marker %s not found in animation: %s.", p_start_marker, name)); + ERR_FAIL_COND_MSG(p_end_marker && !animation->has_marker(p_end_marker), vformat("Marker %s not found in animation: %s.", p_end_marker, name)); + + double start_time = p_start_marker ? animation->get_marker_time(p_start_marker) : -1; + double end_time = p_end_marker ? animation->get_marker_time(p_end_marker) : -1; + + ERR_FAIL_COND_MSG(p_start_marker && p_end_marker && Animation::is_greater_approx(start_time, end_time), vformat("End marker %s is placed earlier than start marker %s in animation: %s.", p_end_marker, p_start_marker, name)); + + if (p_start_marker && Animation::is_less_approx(start_time, 0)) { + WARN_PRINT_ED(vformat("Negative time start marker: %s is invalid in the section, so the start of the animation: %s is used instead.", p_start_marker, playback.current.from->animation->get_name())); + } + if (p_end_marker && Animation::is_less_approx(end_time, 0)) { + WARN_PRINT_ED(vformat("Negative time end marker: %s is invalid in the section, so the end of the animation: %s is used instead.", p_end_marker, playback.current.from->animation->get_name())); + } + + play_section(name, start_time, end_time, p_custom_blend, p_custom_scale, p_from_end); +} + +void AnimationPlayer::play_section(const StringName &p_name, double p_start_time, double p_end_time, double p_custom_blend, float p_custom_scale, bool p_from_end) { + StringName name = p_name; + + if (name == StringName()) { + name = playback.assigned; + } + + ERR_FAIL_COND_MSG(!animation_set.has(name), vformat("Animation not found: %s.", name)); + ERR_FAIL_COND_MSG(p_start_time >= 0 && p_end_time >= 0 && Math::is_equal_approx(p_start_time, p_end_time), "Start time and end time must not equal to each other."); + ERR_FAIL_COND_MSG(p_start_time >= 0 && p_end_time >= 0 && Animation::is_greater_approx(p_start_time, p_end_time), vformat("Start time %f is greater than end time %f.", p_start_time, p_end_time)); + Playback &c = playback; if (c.current.from) { @@ -442,22 +492,27 @@ void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, flo c.current.from = &animation_set[name]; c.current.speed_scale = p_custom_scale; + c.current.start_time = p_start_time; + c.current.end_time = p_end_time; + + double start = get_section_start_time(); + double end = get_section_end_time(); if (!end_reached) { playback_queue.clear(); } if (c.assigned != name) { // Reset. - c.current.pos = p_from_end ? c.current.from->animation->get_length() : 0; + c.current.pos = p_from_end ? end : start; c.assigned = name; emit_signal(SNAME("current_animation_changed"), c.assigned); } else { - if (p_from_end && Math::is_zero_approx(c.current.pos)) { + if (p_from_end && Math::is_equal_approx(c.current.pos, start)) { // Animation reset but played backwards, set position to the end. - seek_internal(c.current.from->animation->get_length(), true, true, true); - } else if (!p_from_end && Math::is_equal_approx(c.current.pos, (double)c.current.from->animation->get_length())) { + seek_internal(end, true, true, true); + } else if (!p_from_end && Math::is_equal_approx(c.current.pos, end)) { // Animation resumed but already ended, set position to the beginning. - seek_internal(0, true, true, true); + seek_internal(start, true, true, true); } else if (playing) { return; } @@ -551,6 +606,8 @@ void AnimationPlayer::set_assigned_animation(const String &p_animation) { ERR_FAIL_COND_MSG(!animation_set.has(p_animation), vformat("Animation not found: %s.", p_animation)); playback.current.pos = 0; playback.current.from = &animation_set[p_animation]; + playback.current.start_time = -1; + playback.current.end_time = -1; playback.assigned = p_animation; emit_signal(SNAME("current_animation_changed"), playback.assigned); } @@ -603,6 +660,12 @@ void AnimationPlayer::seek_internal(double p_time, bool p_update, bool p_update_ } } + double start = get_section_start_time(); + double end = get_section_end_time(); + + // Clamp the seek position. + p_time = CLAMP(p_time, start, end); + playback.seeked = true; playback.internal_seeked = p_is_internal_seek; @@ -641,6 +704,55 @@ double AnimationPlayer::get_current_animation_length() const { return playback.current.from->animation->get_length(); } +void AnimationPlayer::set_section_with_markers(const StringName &p_start_marker, const StringName &p_end_marker) { + ERR_FAIL_NULL_MSG(playback.current.from, "AnimationPlayer has no current animation."); + ERR_FAIL_COND_MSG(p_start_marker == p_end_marker && p_start_marker, vformat("Start marker and end marker cannot be the same marker: %s.", p_start_marker)); + ERR_FAIL_COND_MSG(p_start_marker && !playback.current.from->animation->has_marker(p_start_marker), vformat("Marker %s not found in animation: %s.", p_start_marker, playback.current.from->animation->get_name())); + ERR_FAIL_COND_MSG(p_end_marker && !playback.current.from->animation->has_marker(p_end_marker), vformat("Marker %s not found in animation: %s.", p_end_marker, playback.current.from->animation->get_name())); + double start_time = p_start_marker ? playback.current.from->animation->get_marker_time(p_start_marker) : -1; + double end_time = p_end_marker ? playback.current.from->animation->get_marker_time(p_end_marker) : -1; + if (p_start_marker && Animation::is_less_approx(start_time, 0)) { + WARN_PRINT_ONCE_ED(vformat("Marker %s time must be positive in animation: %s.", p_start_marker, playback.current.from->animation->get_name())); + } + if (p_end_marker && Animation::is_less_approx(end_time, 0)) { + WARN_PRINT_ONCE_ED(vformat("Marker %s time must be positive in animation: %s.", p_end_marker, playback.current.from->animation->get_name())); + } + set_section(start_time, end_time); +} + +void AnimationPlayer::set_section(double p_start_time, double p_end_time) { + ERR_FAIL_NULL_MSG(playback.current.from, "AnimationPlayer has no current animation."); + ERR_FAIL_COND_MSG(Animation::is_greater_or_equal_approx(p_start_time, 0) && Animation::is_greater_or_equal_approx(p_end_time, 0) && Animation::is_greater_or_equal_approx(p_start_time, p_end_time), vformat("Start time %f is greater than end time %f.", p_start_time, p_end_time)); + playback.current.start_time = p_start_time; + playback.current.end_time = p_end_time; + playback.current.pos = CLAMP(playback.current.pos, get_section_start_time(), get_section_end_time()); +} + +void AnimationPlayer::reset_section() { + playback.current.start_time = -1; + playback.current.end_time = -1; +} + +double AnimationPlayer::get_section_start_time() const { + ERR_FAIL_NULL_V_MSG(playback.current.from, playback.current.start_time, "AnimationPlayer has no current animation."); + if (Animation::is_less_approx(playback.current.start_time, 0) || playback.current.start_time > playback.current.from->animation->get_length()) { + return 0; + } + return playback.current.start_time; +} + +double AnimationPlayer::get_section_end_time() const { + ERR_FAIL_NULL_V_MSG(playback.current.from, playback.current.end_time, "AnimationPlayer has no current animation."); + if (Animation::is_less_approx(playback.current.end_time, 0) || playback.current.end_time > playback.current.from->animation->get_length()) { + return playback.current.from->animation->get_length(); + } + return playback.current.end_time; +} + +bool AnimationPlayer::has_section() const { + return Animation::is_greater_or_equal_approx(playback.current.start_time, 0) || Animation::is_greater_or_equal_approx(playback.current.end_time, 0); +} + void AnimationPlayer::set_autoplay(const String &p_name) { if (is_inside_tree() && !Engine::get_singleton()->is_editor_hint()) { WARN_PRINT("Setting autoplay after the node has been added to the scene has no effect."); @@ -665,13 +777,14 @@ void AnimationPlayer::_stop_internal(bool p_reset, bool p_keep_state) { _clear_caches(); Playback &c = playback; // c.blend.clear(); + double start = get_section_start_time(); if (p_reset) { c.blend.clear(); if (p_keep_state) { - c.current.pos = 0; + c.current.pos = start; } else { is_stopping = true; - seek_internal(0, true, true, true); + seek_internal(start, true, true, true); is_stopping = false; } c.current.from = nullptr; @@ -763,20 +876,6 @@ Tween::EaseType AnimationPlayer::get_auto_capture_ease_type() const { return auto_capture_ease_type; } -#ifdef TOOLS_ENABLED -void AnimationPlayer::get_argument_options(const StringName &p_function, int p_idx, List *r_options) const { - const String pf = p_function; - if (p_idx == 0 && (pf == "play" || pf == "play_backwards" || pf == "has_animation" || pf == "queue")) { - List al; - get_animation_list(&al); - for (const StringName &name : al) { - r_options->push_back(String(name).quote()); - } - } - AnimationMixer::get_argument_options(p_function, p_idx, r_options); -} -#endif - void AnimationPlayer::_animation_removed(const StringName &p_name, const StringName &p_library) { AnimationMixer::_animation_removed(p_name, p_library); @@ -863,7 +962,11 @@ void AnimationPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_auto_capture_ease_type"), &AnimationPlayer::get_auto_capture_ease_type); ClassDB::bind_method(D_METHOD("play", "name", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false)); + ClassDB::bind_method(D_METHOD("play_section_with_markers", "name", "start_marker", "end_marker", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play_section_with_markers, DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false)); + ClassDB::bind_method(D_METHOD("play_section", "name", "start_time", "end_time", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play_section, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(-1), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false)); ClassDB::bind_method(D_METHOD("play_backwards", "name", "custom_blend"), &AnimationPlayer::play_backwards, DEFVAL(StringName()), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("play_section_with_markers_backwards", "name", "start_marker", "end_marker", "custom_blend"), &AnimationPlayer::play_section_with_markers_backwards, DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("play_section_backwards", "name", "start_time", "end_time", "custom_blend"), &AnimationPlayer::play_section_backwards, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(-1), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("play_with_capture", "name", "duration", "custom_blend", "custom_speed", "from_end", "trans_type", "ease_type"), &AnimationPlayer::play_with_capture, DEFVAL(StringName()), DEFVAL(-1.0), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false), DEFVAL(Tween::TRANS_LINEAR), DEFVAL(Tween::EASE_IN)); ClassDB::bind_method(D_METHOD("pause"), &AnimationPlayer::pause); ClassDB::bind_method(D_METHOD("stop", "keep_state"), &AnimationPlayer::stop, DEFVAL(false)); @@ -893,6 +996,14 @@ void AnimationPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_current_animation_position"), &AnimationPlayer::get_current_animation_position); ClassDB::bind_method(D_METHOD("get_current_animation_length"), &AnimationPlayer::get_current_animation_length); + ClassDB::bind_method(D_METHOD("set_section_with_markers", "start_marker", "end_marker"), &AnimationPlayer::set_section_with_markers, DEFVAL(StringName()), DEFVAL(StringName())); + ClassDB::bind_method(D_METHOD("set_section", "start_time", "end_time"), &AnimationPlayer::set_section, DEFVAL(-1), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("reset_section"), &AnimationPlayer::reset_section); + + ClassDB::bind_method(D_METHOD("get_section_start_time"), &AnimationPlayer::get_section_start_time); + ClassDB::bind_method(D_METHOD("get_section_end_time"), &AnimationPlayer::get_section_end_time); + ClassDB::bind_method(D_METHOD("has_section"), &AnimationPlayer::has_section); + ClassDB::bind_method(D_METHOD("seek", "seconds", "update", "update_only"), &AnimationPlayer::seek, DEFVAL(false), DEFVAL(false)); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "current_animation", PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_EDITOR), "set_current_animation", "get_current_animation"); diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h index e05a2c99350..3223e2522de 100644 --- a/scene/animation/animation_player.h +++ b/scene/animation/animation_player.h @@ -68,6 +68,8 @@ class AnimationPlayer : public AnimationMixer { AnimationData *from = nullptr; double pos = 0.0; float speed_scale = 1.0; + double start_time = 0.0; + double end_time = 0.0; }; struct Blend { @@ -177,7 +179,11 @@ class AnimationPlayer : public AnimationMixer { Tween::EaseType get_auto_capture_ease_type() const; void play(const StringName &p_name = StringName(), double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); + void play_section_with_markers(const StringName &p_name = StringName(), const StringName &p_start_marker = StringName(), const StringName &p_end_marker = StringName(), double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); + void play_section(const StringName &p_name = StringName(), double p_start_time = -1, double p_end_time = -1, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); void play_backwards(const StringName &p_name = StringName(), double p_custom_blend = -1); + void play_section_with_markers_backwards(const StringName &p_name = StringName(), const StringName &p_start_marker = StringName(), const StringName &p_end_marker = StringName(), double p_custom_blend = -1); + void play_section_backwards(const StringName &p_name = StringName(), double p_start_time = -1, double p_end_time = -1, double p_custom_blend = -1); void play_with_capture(const StringName &p_name = StringName(), double p_duration = -1.0, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN); void queue(const StringName &p_name); Vector get_queue(); @@ -207,9 +213,13 @@ class AnimationPlayer : public AnimationMixer { double get_current_animation_position() const; double get_current_animation_length() const; -#ifdef TOOLS_ENABLED - void get_argument_options(const StringName &p_function, int p_idx, List *r_options) const override; -#endif + void set_section_with_markers(const StringName &p_start_marker = StringName(), const StringName &p_end_marker = StringName()); + void set_section(double p_start_time = -1, double p_end_time = -1); + void reset_section(); + + double get_section_start_time() const; + double get_section_end_time() const; + bool has_section() const; virtual void advance(double p_time) override; diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index 1dac4b97ad9..57a4e35f7a7 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -62,6 +62,23 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) { compression.pages[i].time_offset = page["time_offset"]; } compression.enabled = true; + return true; + } else if (prop_name == SNAME("markers")) { + Array markers = p_value; + for (const Dictionary marker : markers) { + ERR_FAIL_COND_V(!marker.has("name"), false); + ERR_FAIL_COND_V(!marker.has("time"), false); + StringName marker_name = marker["name"]; + double time = marker["time"]; + _marker_insert(time, marker_names, MarkerKey(time, marker_name)); + marker_times.insert(marker_name, time); + Color color = Color(1, 1, 1); + if (marker.has("color")) { + color = marker["color"]; + } + marker_colors.insert(marker_name, color); + } + return true; } else if (prop_name.begins_with("tracks/")) { int track = prop_name.get_slicec('/', 1).to_int(); @@ -470,6 +487,18 @@ bool Animation::_get(const StringName &p_name, Variant &r_ret) const { r_ret = comp; return true; + } else if (prop_name == SNAME("markers")) { + Array markers; + + for (HashMap::ConstIterator E = marker_times.begin(); E; ++E) { + Dictionary d; + d["name"] = E->key; + d["time"] = E->value; + d["color"] = marker_colors[E->key]; + markers.push_back(d); + } + + r_ret = markers; } else if (prop_name == "length") { r_ret = length; } else if (prop_name == "loop_mode") { @@ -839,6 +868,7 @@ void Animation::_get_property_list(List *p_list) const { if (compression.enabled) { p_list->push_back(PropertyInfo(Variant::DICTIONARY, "_compression", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); } + p_list->push_back(PropertyInfo(Variant::ARRAY, "markers", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); for (int i = 0; i < tracks.size(); i++) { p_list->push_back(PropertyInfo(Variant::STRING, "tracks/" + itos(i) + "/type", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); p_list->push_back(PropertyInfo(Variant::BOOL, "tracks/" + itos(i) + "/imported", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); @@ -1087,6 +1117,27 @@ int Animation::_insert(double p_time, T &p_keys, const V &p_value) { return -1; } +int Animation::_marker_insert(double p_time, Vector &p_keys, const MarkerKey &p_value) { + int idx = p_keys.size(); + + while (true) { + // Condition for replacement. + if (idx > 0 && Math::is_equal_approx((double)p_keys[idx - 1].time, p_time)) { + p_keys.write[idx - 1] = p_value; + return idx - 1; + + // Condition for insert. + } else if (idx == 0 || p_keys[idx - 1].time < p_time) { + p_keys.insert(idx, p_value); + return idx; + } + + idx--; + } + + return -1; +} + template void Animation::_clear(T &p_keys) { p_keys.clear(); @@ -3163,6 +3214,90 @@ void Animation::track_get_key_indices_in_range(int p_track, double p_time, doubl } } +void Animation::add_marker(const StringName &p_name, double p_time) { + int idx = _find(marker_names, p_time); + + if (idx >= 0 && idx < marker_names.size() && Math::is_equal_approx(p_time, marker_names[idx].time)) { + marker_times.erase(marker_names[idx].name); + marker_colors.erase(marker_names[idx].name); + marker_names.write[idx].name = p_name; + marker_times.insert(p_name, p_time); + marker_colors.insert(p_name, Color(1, 1, 1)); + } else { + _marker_insert(p_time, marker_names, MarkerKey(p_time, p_name)); + marker_times.insert(p_name, p_time); + marker_colors.insert(p_name, Color(1, 1, 1)); + } +} + +void Animation::remove_marker(const StringName &p_name) { + HashMap::Iterator E = marker_times.find(p_name); + ERR_FAIL_COND(!E); + int idx = _find(marker_names, E->value); + bool success = idx >= 0 && idx < marker_names.size() && Math::is_equal_approx(marker_names[idx].time, E->value); + ERR_FAIL_COND(!success); + marker_names.remove_at(idx); + marker_times.remove(E); + marker_colors.erase(p_name); +} + +bool Animation::has_marker(const StringName &p_name) const { + return marker_times.has(p_name); +} + +StringName Animation::get_marker_at_time(double p_time) const { + int idx = _find(marker_names, p_time); + + if (idx >= 0 && idx < marker_names.size() && Math::is_equal_approx(marker_names[idx].time, p_time)) { + return marker_names[idx].name; + } + + return StringName(); +} + +StringName Animation::get_next_marker(double p_time) const { + int idx = _find(marker_names, p_time); + + if (idx >= -1 && idx < marker_names.size() - 1) { + // _find ensures that the time at idx is always the closest time to p_time that is also smaller to it. + // So we add 1 to get the next marker. + return marker_names[idx + 1].name; + } + return StringName(); +} + +StringName Animation::get_prev_marker(double p_time) const { + int idx = _find(marker_names, p_time); + + if (idx >= 0 && idx < marker_names.size()) { + return marker_names[idx].name; + } + return StringName(); +} + +double Animation::get_marker_time(const StringName &p_name) const { + ERR_FAIL_COND_V(!marker_times.has(p_name), -1); + return marker_times.get(p_name); +} + +PackedStringArray Animation::get_marker_names() const { + PackedStringArray names; + // We iterate on marker_names so the result is sorted by time. + for (const MarkerKey &marker_name : marker_names) { + names.push_back(marker_name.name); + } + return names; +} + +Color Animation::get_marker_color(const StringName &p_name) const { + ERR_FAIL_COND_V(!marker_colors.has(p_name), Color()); + return marker_colors[p_name]; +} + +void Animation::set_marker_color(const StringName &p_name, const Color &p_color) { + marker_colors[p_name] = p_color; +} + Vector Animation::method_track_get_params(int p_track, int p_key_idx) const { ERR_FAIL_INDEX_V(p_track, tracks.size(), Vector()); Track *t = tracks[p_track]; @@ -3894,6 +4029,17 @@ void Animation::_bind_methods() { ClassDB::bind_method(D_METHOD("animation_track_set_key_animation", "track_idx", "key_idx", "animation"), &Animation::animation_track_set_key_animation); ClassDB::bind_method(D_METHOD("animation_track_get_key_animation", "track_idx", "key_idx"), &Animation::animation_track_get_key_animation); + ClassDB::bind_method(D_METHOD("add_marker", "name", "time"), &Animation::add_marker); + ClassDB::bind_method(D_METHOD("remove_marker", "name"), &Animation::remove_marker); + ClassDB::bind_method(D_METHOD("has_marker", "name"), &Animation::has_marker); + ClassDB::bind_method(D_METHOD("get_marker_at_time", "time"), &Animation::get_marker_at_time); + ClassDB::bind_method(D_METHOD("get_next_marker", "time"), &Animation::get_next_marker); + ClassDB::bind_method(D_METHOD("get_prev_marker", "time"), &Animation::get_prev_marker); + ClassDB::bind_method(D_METHOD("get_marker_time", "name"), &Animation::get_marker_time); + ClassDB::bind_method(D_METHOD("get_marker_names"), &Animation::get_marker_names); + ClassDB::bind_method(D_METHOD("get_marker_color", "name"), &Animation::get_marker_color); + ClassDB::bind_method(D_METHOD("set_marker_color", "name", "color"), &Animation::set_marker_color); + ClassDB::bind_method(D_METHOD("set_length", "time_sec"), &Animation::set_length); ClassDB::bind_method(D_METHOD("get_length"), &Animation::get_length); diff --git a/scene/resources/animation.h b/scene/resources/animation.h index 0c29790ea40..618dc9ca17c 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -237,6 +237,20 @@ class Animation : public Resource { } }; + /* Marker */ + + struct MarkerKey { + double time; + StringName name; + MarkerKey(double p_time, const StringName &p_name) : + time(p_time), name(p_name) {} + MarkerKey() = default; + }; + + Vector marker_names; // time -> name + HashMap marker_times; // name -> time + HashMap marker_colors; // name -> color + Vector tracks; template @@ -245,6 +259,8 @@ class Animation : public Resource { template int _insert(double p_time, T &p_keys, const V &p_value); + int _marker_insert(double p_time, Vector &p_keys, const MarkerKey &p_value); + template inline int _find(const Vector &p_keys, double p_time, bool p_backward = false, bool p_limit = false) const; @@ -501,6 +517,17 @@ class Animation : public Resource { void track_get_key_indices_in_range(int p_track, double p_time, double p_delta, List *p_indices, Animation::LoopedFlag p_looped_flag = Animation::LOOPED_FLAG_NONE) const; + void add_marker(const StringName &p_name, double p_time); + void remove_marker(const StringName &p_name); + bool has_marker(const StringName &p_name) const; + StringName get_marker_at_time(double p_time) const; + StringName get_next_marker(double p_time) const; + StringName get_prev_marker(double p_time) const; + double get_marker_time(const StringName &p_time) const; + PackedStringArray get_marker_names() const; + Color get_marker_color(const StringName &p_name) const; + void set_marker_color(const StringName &p_name, const Color &p_color); + void set_length(real_t p_length); real_t get_length() const; From d720eb80e176bf7455f37cfff9e336c95bc911f4 Mon Sep 17 00:00:00 2001 From: Patrick Owen Date: Sat, 28 Sep 2024 19:02:06 -0400 Subject: [PATCH 13/30] Clamp UV-coordinates to centers of outermost texels when configured to do so In addition, fix region_filter_clip_enabled documentation to be consistent with AtlasTexture.xml, since that is the option whose behavior was fixed --- doc/classes/Sprite2D.xml | 2 +- drivers/gles3/shaders/canvas.glsl | 3 ++- servers/rendering/renderer_rd/shaders/canvas.glsl | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/classes/Sprite2D.xml b/doc/classes/Sprite2D.xml index d73cb02d94a..239c4dcf095 100644 --- a/doc/classes/Sprite2D.xml +++ b/doc/classes/Sprite2D.xml @@ -76,7 +76,7 @@ If [code]true[/code], texture is cut from a larger atlas texture. See [member region_rect]. - If [code]true[/code], the outermost pixels get blurred out. [member region_enabled] must be [code]true[/code]. + If [code]true[/code], the area outside of the [member region_rect] is clipped to avoid bleeding of the surrounding texture pixels. [member region_enabled] must be [code]true[/code]. The region of the atlas texture to display. [member region_enabled] must be [code]true[/code]. diff --git a/drivers/gles3/shaders/canvas.glsl b/drivers/gles3/shaders/canvas.glsl index 76881c8032d..5e7fb3b3389 100644 --- a/drivers/gles3/shaders/canvas.glsl +++ b/drivers/gles3/shaders/canvas.glsl @@ -579,7 +579,8 @@ void main() { #endif if (bool(read_draw_data_flags & FLAGS_CLIP_RECT_UV)) { - uv = clamp(uv, read_draw_data_src_rect.xy, read_draw_data_src_rect.xy + abs(read_draw_data_src_rect.zw)); + vec2 half_texpixel = read_draw_data_color_texture_pixel_size * 0.5; + uv = clamp(uv, read_draw_data_src_rect.xy + half_texpixel, read_draw_data_src_rect.xy + abs(read_draw_data_src_rect.zw) - half_texpixel); } #endif diff --git a/servers/rendering/renderer_rd/shaders/canvas.glsl b/servers/rendering/renderer_rd/shaders/canvas.glsl index 704aafdfa5d..fd0cd5bfad4 100644 --- a/servers/rendering/renderer_rd/shaders/canvas.glsl +++ b/servers/rendering/renderer_rd/shaders/canvas.glsl @@ -493,7 +493,8 @@ void main() { #endif if (bool(draw_data.flags & FLAGS_CLIP_RECT_UV)) { - uv = clamp(uv, draw_data.src_rect.xy, draw_data.src_rect.xy + abs(draw_data.src_rect.zw)); + vec2 half_texpixel = draw_data.color_texture_pixel_size * 0.5; + uv = clamp(uv, draw_data.src_rect.xy + half_texpixel, draw_data.src_rect.xy + abs(draw_data.src_rect.zw) - half_texpixel); } #endif From 146ba4106f459fd37d130a34da02ef737ac166e4 Mon Sep 17 00:00:00 2001 From: Gergely Kis Date: Thu, 26 Sep 2024 21:46:48 +0200 Subject: [PATCH 14/30] Move Vulkan includes to a central godot_vulkan.h header Also fixes Vulkan build problem with recent Clang. --- drivers/vulkan/godot_vulkan.h | 42 +++++++++++++++++++ .../vulkan/rendering_context_driver_vulkan.h | 6 +-- .../vulkan/rendering_device_driver_vulkan.h | 6 +-- drivers/vulkan/vulkan_hooks.h | 6 +-- ...endering_context_driver_vulkan_android.cpp | 6 +-- platform/ios/display_server_ios.h | 6 +-- platform/ios/os_ios.mm | 6 +-- ...endering_context_driver_vulkan_wayland.cpp | 6 +-- .../rendering_context_driver_vulkan_x11.cpp | 6 +-- ...endering_context_driver_vulkan_windows.cpp | 6 +-- ...VKEnumStringHelper-use-godot-vulkan.patch} | 10 ++--- ...-volk.patch => VMA-use-godot-vulkan.patch} | 15 +++---- thirdparty/vulkan/vk_enum_string_helper.h | 6 +-- thirdparty/vulkan/vk_mem_alloc.h | 6 +-- 14 files changed, 64 insertions(+), 69 deletions(-) create mode 100644 drivers/vulkan/godot_vulkan.h rename thirdparty/vulkan/patches/{VKEnumStringHelper-use-volk.patch => VKEnumStringHelper-use-godot-vulkan.patch} (73%) rename thirdparty/vulkan/patches/{VMA-use-volk.patch => VMA-use-godot-vulkan.patch} (54%) diff --git a/drivers/vulkan/godot_vulkan.h b/drivers/vulkan/godot_vulkan.h new file mode 100644 index 00000000000..f911c5520a8 --- /dev/null +++ b/drivers/vulkan/godot_vulkan.h @@ -0,0 +1,42 @@ +/**************************************************************************/ +/* godot_vulkan.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GODOT_VULKAN_H +#define GODOT_VULKAN_H + +#ifdef USE_VOLK +#include +#else +#include +#define VK_NO_STDINT_H +#include +#endif + +#endif // GODOT_VULKAN_H diff --git a/drivers/vulkan/rendering_context_driver_vulkan.h b/drivers/vulkan/rendering_context_driver_vulkan.h index f9352d617b8..26de3862061 100644 --- a/drivers/vulkan/rendering_context_driver_vulkan.h +++ b/drivers/vulkan/rendering_context_driver_vulkan.h @@ -40,11 +40,7 @@ #define VK_TRACK_DEVICE_MEMORY #endif -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" class RenderingContextDriverVulkan : public RenderingContextDriver { public: diff --git a/drivers/vulkan/rendering_device_driver_vulkan.h b/drivers/vulkan/rendering_device_driver_vulkan.h index 81f4256941e..cc15c0a0fef 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.h +++ b/drivers/vulkan/rendering_device_driver_vulkan.h @@ -43,11 +43,7 @@ #endif #include "thirdparty/vulkan/vk_mem_alloc.h" -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" // Design principles: // - Vulkan structs are zero-initialized and fields not requiring a non-zero value are omitted (except in cases where expresivity reasons apply). diff --git a/drivers/vulkan/vulkan_hooks.h b/drivers/vulkan/vulkan_hooks.h index bb30b29cec9..82bcc9a0644 100644 --- a/drivers/vulkan/vulkan_hooks.h +++ b/drivers/vulkan/vulkan_hooks.h @@ -31,11 +31,7 @@ #ifndef VULKAN_HOOKS_H #define VULKAN_HOOKS_H -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" class VulkanHooks { private: diff --git a/platform/android/rendering_context_driver_vulkan_android.cpp b/platform/android/rendering_context_driver_vulkan_android.cpp index a306a121f88..51fb1ca18fa 100644 --- a/platform/android/rendering_context_driver_vulkan_android.cpp +++ b/platform/android/rendering_context_driver_vulkan_android.cpp @@ -32,11 +32,7 @@ #ifdef VULKAN_ENABLED -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" const char *RenderingContextDriverVulkanAndroid::_get_platform_surface_extension() const { return VK_KHR_ANDROID_SURFACE_EXTENSION_NAME; diff --git a/platform/ios/display_server_ios.h b/platform/ios/display_server_ios.h index bbb758074d9..0631b50f0ad 100644 --- a/platform/ios/display_server_ios.h +++ b/platform/ios/display_server_ios.h @@ -41,11 +41,7 @@ #if defined(VULKAN_ENABLED) #import "rendering_context_driver_vulkan_ios.h" -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" #endif // VULKAN_ENABLED #if defined(METAL_ENABLED) diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index 35b87ea6470..590238be77f 100644 --- a/platform/ios/os_ios.mm +++ b/platform/ios/os_ios.mm @@ -56,11 +56,7 @@ #import #if defined(VULKAN_ENABLED) -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" #endif // VULKAN_ENABLED #endif diff --git a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp index 0417ba95eb5..8abcc464baa 100644 --- a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp +++ b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp @@ -32,11 +32,7 @@ #include "rendering_context_driver_vulkan_wayland.h" -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" const char *RenderingContextDriverVulkanWayland::_get_platform_surface_extension() const { return VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME; diff --git a/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp b/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp index 3f505d000c2..cbcf07852ba 100644 --- a/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp +++ b/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp @@ -32,11 +32,7 @@ #include "rendering_context_driver_vulkan_x11.h" -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" const char *RenderingContextDriverVulkanX11::_get_platform_surface_extension() const { return VK_KHR_XLIB_SURFACE_EXTENSION_NAME; diff --git a/platform/windows/rendering_context_driver_vulkan_windows.cpp b/platform/windows/rendering_context_driver_vulkan_windows.cpp index 445388af89f..8ca677fe648 100644 --- a/platform/windows/rendering_context_driver_vulkan_windows.cpp +++ b/platform/windows/rendering_context_driver_vulkan_windows.cpp @@ -34,11 +34,7 @@ #include "rendering_context_driver_vulkan_windows.h" -#ifdef USE_VOLK -#include -#else -#include -#endif +#include "drivers/vulkan/godot_vulkan.h" const char *RenderingContextDriverVulkanWindows::_get_platform_surface_extension() const { return VK_KHR_WIN32_SURFACE_EXTENSION_NAME; diff --git a/thirdparty/vulkan/patches/VKEnumStringHelper-use-volk.patch b/thirdparty/vulkan/patches/VKEnumStringHelper-use-godot-vulkan.patch similarity index 73% rename from thirdparty/vulkan/patches/VKEnumStringHelper-use-volk.patch rename to thirdparty/vulkan/patches/VKEnumStringHelper-use-godot-vulkan.patch index 8517b277d05..6b56d60181c 100644 --- a/thirdparty/vulkan/patches/VKEnumStringHelper-use-volk.patch +++ b/thirdparty/vulkan/patches/VKEnumStringHelper-use-godot-vulkan.patch @@ -1,17 +1,13 @@ diff --git a/thirdparty/vulkan/vk_enum_string_helper.h b/thirdparty/vulkan/vk_enum_string_helper.h -index 9d2af46344..d61dbb1290 100644 +index 8026787ad4..7a54b12a38 100644 --- a/thirdparty/vulkan/vk_enum_string_helper.h +++ b/thirdparty/vulkan/vk_enum_string_helper.h -@@ -13,7 +13,11 @@ +@@ -13,7 +13,7 @@ #ifdef __cplusplus #include #endif -#include -+#ifdef USE_VOLK -+ #include -+#else -+ #include -+#endif ++#include "drivers/vulkan/godot_vulkan.h" static inline const char* string_VkResult(VkResult input_value) { switch (input_value) { case VK_SUCCESS: diff --git a/thirdparty/vulkan/patches/VMA-use-volk.patch b/thirdparty/vulkan/patches/VMA-use-godot-vulkan.patch similarity index 54% rename from thirdparty/vulkan/patches/VMA-use-volk.patch rename to thirdparty/vulkan/patches/VMA-use-godot-vulkan.patch index e2e5ea5ad4f..a6c546e3d86 100644 --- a/thirdparty/vulkan/patches/VMA-use-volk.patch +++ b/thirdparty/vulkan/patches/VMA-use-godot-vulkan.patch @@ -1,17 +1,18 @@ diff --git a/thirdparty/vulkan/vk_mem_alloc.h b/thirdparty/vulkan/vk_mem_alloc.h -index 711f486571..e5eaa80e74 100644 +index 2307325d4e..ecb84094b9 100644 --- a/thirdparty/vulkan/vk_mem_alloc.h +++ b/thirdparty/vulkan/vk_mem_alloc.h -@@ -127,7 +127,11 @@ See documentation chapter: \ref statistics. +@@ -122,12 +122,12 @@ for user-defined purpose without allocating any real GPU memory. + See documentation chapter: \ref statistics. + */ + ++#include "drivers/vulkan/godot_vulkan.h" + + #ifdef __cplusplus extern "C" { #endif -#include -+#ifdef USE_VOLK -+ #include -+#else -+ #include -+#endif #if !defined(VMA_VULKAN_VERSION) #if defined(VK_VERSION_1_3) diff --git a/thirdparty/vulkan/vk_enum_string_helper.h b/thirdparty/vulkan/vk_enum_string_helper.h index 598453e7454..7a54b12a380 100644 --- a/thirdparty/vulkan/vk_enum_string_helper.h +++ b/thirdparty/vulkan/vk_enum_string_helper.h @@ -13,11 +13,7 @@ #ifdef __cplusplus #include #endif -#ifdef USE_VOLK - #include -#else - #include -#endif +#include "drivers/vulkan/godot_vulkan.h" static inline const char* string_VkResult(VkResult input_value) { switch (input_value) { case VK_SUCCESS: diff --git a/thirdparty/vulkan/vk_mem_alloc.h b/thirdparty/vulkan/vk_mem_alloc.h index b39b73b17dd..ecb84094b95 100644 --- a/thirdparty/vulkan/vk_mem_alloc.h +++ b/thirdparty/vulkan/vk_mem_alloc.h @@ -122,16 +122,12 @@ for user-defined purpose without allocating any real GPU memory. See documentation chapter: \ref statistics. */ +#include "drivers/vulkan/godot_vulkan.h" #ifdef __cplusplus extern "C" { #endif -#ifdef USE_VOLK - #include -#else - #include -#endif #if !defined(VMA_VULKAN_VERSION) #if defined(VK_VERSION_1_3) From 07cae26abeb85d9e7e1f7c6c86b6f782e3eec646 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Thu, 26 Sep 2024 08:41:46 -0700 Subject: [PATCH 15/30] Remove the restriction on supported types for Godot Android plugins The Android plugin implementation is updated to use `JavaClassWrapper` which was fixed in https://github.com/godotengine/godot/pull/96182, thus removing the limitation on supported types. Note that `JavaClassWrapper` has also been updated in order to only provide access to public methods and constructor to GDScript. --- platform/android/api/api.cpp | 10 +- platform/android/api/java_class_wrapper.h | 13 +- platform/android/api/jni_singleton.h | 197 +++---------------- platform/android/java_class_wrapper.cpp | 10 +- platform/android/java_godot_lib_jni.cpp | 4 +- platform/android/plugin/godot_plugin_jni.cpp | 25 +-- 6 files changed, 51 insertions(+), 208 deletions(-) diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp index 6920f801e52..078b9ab748f 100644 --- a/platform/android/api/api.cpp +++ b/platform/android/api/api.cpp @@ -41,13 +41,11 @@ static JavaClassWrapper *java_class_wrapper = nullptr; void register_android_api() { #if !defined(ANDROID_ENABLED) - // On Android platforms, the `java_class_wrapper` instantiation and the - // `JNISingleton` registration occurs in + // On Android platforms, the `java_class_wrapper` instantiation occurs in // `platform/android/java_godot_lib_jni.cpp#Java_org_godotengine_godot_GodotLib_setup` - java_class_wrapper = memnew(JavaClassWrapper); // Dummy - GDREGISTER_CLASS(JNISingleton); + java_class_wrapper = memnew(JavaClassWrapper); #endif - + GDREGISTER_CLASS(JNISingleton); GDREGISTER_CLASS(JavaClass); GDREGISTER_CLASS(JavaObject); GDREGISTER_CLASS(JavaClassWrapper); @@ -108,7 +106,7 @@ Ref JavaObject::get_java_class() const { JavaClassWrapper *JavaClassWrapper::singleton = nullptr; -Ref JavaClassWrapper::wrap(const String &) { +Ref JavaClassWrapper::_wrap(const String &, bool) { return Ref(); } diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index 71f9c32318f..c74cef8dd0b 100644 --- a/platform/android/api/java_class_wrapper.h +++ b/platform/android/api/java_class_wrapper.h @@ -262,6 +262,8 @@ class JavaClassWrapper : public Object { bool _get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, String &strsig); #endif + Ref _wrap(const String &p_class, bool p_allow_private_methods_access); + static JavaClassWrapper *singleton; protected: @@ -270,15 +272,14 @@ class JavaClassWrapper : public Object { public: static JavaClassWrapper *get_singleton() { return singleton; } - Ref wrap(const String &p_class); + Ref wrap(const String &p_class) { + return _wrap(p_class, false); + } #ifdef ANDROID_ENABLED - Ref wrap_jclass(jclass p_class); - - JavaClassWrapper(jobject p_activity = nullptr); -#else - JavaClassWrapper(); + Ref wrap_jclass(jclass p_class, bool p_allow_private_methods_access = false); #endif + JavaClassWrapper(); }; #endif // JAVA_CLASS_WRAPPER_H diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index 06afc4eb782..5e940819bcc 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -31,193 +31,53 @@ #ifndef JNI_SINGLETON_H #define JNI_SINGLETON_H +#include "java_class_wrapper.h" + #include "core/config/engine.h" #include "core/variant/variant.h" -#ifdef ANDROID_ENABLED -#include "jni_utils.h" -#endif - class JNISingleton : public Object { GDCLASS(JNISingleton, Object); -#ifdef ANDROID_ENABLED struct MethodData { - jmethodID method; Variant::Type ret_type; Vector argtypes; }; - jobject instance; RBMap method_map; -#endif + Ref wrapped_object; public: virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override { -#ifdef ANDROID_ENABLED - RBMap::Element *E = method_map.find(p_method); - - // Check the method we're looking for is in the JNISingleton map and that - // the arguments match. - bool call_error = !E || E->get().argtypes.size() != p_argcount; - if (!call_error) { - for (int i = 0; i < p_argcount; i++) { - if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) { - call_error = true; - break; + if (wrapped_object.is_valid()) { + RBMap::Element *E = method_map.find(p_method); + + // Check the method we're looking for is in the JNISingleton map and that + // the arguments match. + bool call_error = !E || E->get().argtypes.size() != p_argcount; + if (!call_error) { + for (int i = 0; i < p_argcount; i++) { + if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) { + call_error = true; + break; + } } } - } - - if (call_error) { - // The method is not in this map, defaulting to the regular instance calls. - return Object::callp(p_method, p_args, p_argcount, r_error); - } - - ERR_FAIL_NULL_V(instance, Variant()); - - r_error.error = Callable::CallError::CALL_OK; - - jvalue *v = nullptr; - if (p_argcount) { - v = (jvalue *)alloca(sizeof(jvalue) * p_argcount); - } - - JNIEnv *env = get_jni_env(); - - int res = env->PushLocalFrame(16); - - ERR_FAIL_COND_V(res != 0, Variant()); - - List to_erase; - for (int i = 0; i < p_argcount; i++) { - jvalret vr = _variant_to_jvalue(env, E->get().argtypes[i], p_args[i]); - v[i] = vr.val; - if (vr.obj) { - to_erase.push_back(vr.obj); + if (!call_error) { + return wrapped_object->callp(p_method, p_args, p_argcount, r_error); } } - Variant ret; - - switch (E->get().ret_type) { - case Variant::NIL: { - env->CallVoidMethodA(instance, E->get().method, v); - } break; - case Variant::BOOL: { - ret = env->CallBooleanMethodA(instance, E->get().method, v) == JNI_TRUE; - } break; - case Variant::INT: { - ret = env->CallIntMethodA(instance, E->get().method, v); - } break; - case Variant::FLOAT: { - ret = env->CallFloatMethodA(instance, E->get().method, v); - } break; - case Variant::STRING: { - jobject o = env->CallObjectMethodA(instance, E->get().method, v); - ret = jstring_to_string((jstring)o, env); - env->DeleteLocalRef(o); - } break; - case Variant::PACKED_STRING_ARRAY: { - jobjectArray arr = (jobjectArray)env->CallObjectMethodA(instance, E->get().method, v); - - ret = _jobject_to_variant(env, arr); - - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_INT32_ARRAY: { - jintArray arr = (jintArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector sarr; - sarr.resize(fCount); - - int *w = sarr.ptrw(); - env->GetIntArrayRegion(arr, 0, fCount, w); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_INT64_ARRAY: { - jlongArray arr = (jlongArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector sarr; - sarr.resize(fCount); - - int64_t *w = sarr.ptrw(); - env->GetLongArrayRegion(arr, 0, fCount, w); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_FLOAT32_ARRAY: { - jfloatArray arr = (jfloatArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector sarr; - sarr.resize(fCount); - - float *w = sarr.ptrw(); - env->GetFloatArrayRegion(arr, 0, fCount, w); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_FLOAT64_ARRAY: { - jdoubleArray arr = (jdoubleArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector sarr; - sarr.resize(fCount); - - double *w = sarr.ptrw(); - env->GetDoubleArrayRegion(arr, 0, fCount, w); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - case Variant::DICTIONARY: { - jobject obj = env->CallObjectMethodA(instance, E->get().method, v); - ret = _jobject_to_variant(env, obj); - env->DeleteLocalRef(obj); - - } break; - case Variant::OBJECT: { - jobject obj = env->CallObjectMethodA(instance, E->get().method, v); - ret = _jobject_to_variant(env, obj); - env->DeleteLocalRef(obj); - } break; - default: { - env->PopLocalFrame(nullptr); - ERR_FAIL_V(Variant()); - } break; - } - - while (to_erase.size()) { - env->DeleteLocalRef(to_erase.front()->get()); - to_erase.pop_front(); - } - - env->PopLocalFrame(nullptr); - - return ret; -#else // ANDROID_ENABLED - - // Defaulting to the regular instance calls. return Object::callp(p_method, p_args, p_argcount, r_error); -#endif } -#ifdef ANDROID_ENABLED - jobject get_instance() const { - return instance; + Ref get_wrapped_object() const { + return wrapped_object; } - void set_instance(jobject p_instance) { - instance = p_instance; - } - - void add_method(const StringName &p_name, jmethodID p_method, const Vector &p_args, Variant::Type p_ret_type) { + void add_method(const StringName &p_name, const Vector &p_args, Variant::Type p_ret_type) { MethodData md; - md.method = p_method; md.argtypes = p_args; md.ret_type = p_ret_type; method_map[p_name] = md; @@ -232,24 +92,15 @@ class JNISingleton : public Object { ADD_SIGNAL(mi); } -#endif + JNISingleton() {} - JNISingleton() { -#ifdef ANDROID_ENABLED - instance = nullptr; -#endif + JNISingleton(const Ref &p_wrapped_object) { + wrapped_object = p_wrapped_object; } ~JNISingleton() { -#ifdef ANDROID_ENABLED method_map.clear(); - if (instance) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL(env); - - env->DeleteGlobalRef(instance); - } -#endif + wrapped_object.unref(); } }; diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index c92717e9227..6bedbfd157a 100644 --- a/platform/android/java_class_wrapper.cpp +++ b/platform/android/java_class_wrapper.cpp @@ -1120,7 +1120,7 @@ bool JavaClass::_convert_object_to_variant(JNIEnv *env, jobject obj, Variant &va return false; } -Ref JavaClassWrapper::wrap(const String &p_class) { +Ref JavaClassWrapper::_wrap(const String &p_class, bool p_allow_private_methods_access) { String class_name_dots = p_class.replace("/", "."); if (class_cache.has(class_name_dots)) { return class_cache[class_name_dots]; @@ -1175,7 +1175,7 @@ Ref JavaClassWrapper::wrap(const String &p_class) { jint mods = env->CallIntMethod(obj, is_constructor ? Constructor_getModifiers : Method_getModifiers); - if (!(mods & 0x0001)) { + if (!(mods & 0x0001) && (is_constructor || !p_allow_private_methods_access)) { env->DeleteLocalRef(obj); continue; //not public bye } @@ -1336,7 +1336,7 @@ Ref JavaClassWrapper::wrap(const String &p_class) { return java_class; } -Ref JavaClassWrapper::wrap_jclass(jclass p_class) { +Ref JavaClassWrapper::wrap_jclass(jclass p_class, bool p_allow_private_methods_access) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, Ref()); @@ -1344,12 +1344,12 @@ Ref JavaClassWrapper::wrap_jclass(jclass p_class) { String class_name_string = jstring_to_string(class_name, env); env->DeleteLocalRef(class_name); - return wrap(class_name_string); + return _wrap(class_name_string, p_allow_private_methods_access); } JavaClassWrapper *JavaClassWrapper::singleton = nullptr; -JavaClassWrapper::JavaClassWrapper(jobject p_activity) { +JavaClassWrapper::JavaClassWrapper() { singleton = this; JNIEnv *env = get_jni_env(); diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 6086f67a1e7..1a256959cdb 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -32,7 +32,6 @@ #include "android_input_handler.h" #include "api/java_class_wrapper.h" -#include "api/jni_singleton.h" #include "dir_access_jandroid.h" #include "display_server_android.h" #include "file_access_android.h" @@ -209,8 +208,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env TTS_Android::setup(p_godot_tts); - java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity())); - GDREGISTER_CLASS(JNISingleton); + java_class_wrapper = memnew(JavaClassWrapper); return true; } diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index 75c8dd95281..acb18cc5c5c 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -30,6 +30,7 @@ #include "godot_plugin_jni.h" +#include "api/java_class_wrapper.h" #include "api/jni_singleton.h" #include "jni_utils.h" #include "string_android.h" @@ -57,11 +58,15 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeR ERR_FAIL_COND_V(jni_singletons.has(singname), false); - JNISingleton *s = (JNISingleton *)ClassDB::instantiate("JNISingleton"); - s->set_instance(env->NewGlobalRef(obj)); - jni_singletons[singname] = s; + jclass java_class = env->GetObjectClass(obj); + Ref java_class_wrapped = JavaClassWrapper::get_singleton()->wrap_jclass(java_class, true); + env->DeleteLocalRef(java_class); - Engine::get_singleton()->add_singleton(Engine::Singleton(singname, s)); + Ref plugin_object = memnew(JavaObject(java_class_wrapped, obj)); + JNISingleton *plugin_singleton = memnew(JNISingleton(plugin_object)); + jni_singletons[singname] = plugin_singleton; + + Engine::get_singleton()->add_singleton(Engine::Singleton(singname, plugin_singleton)); return true; } @@ -75,7 +80,6 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis String mname = jstring_to_string(name, env); String retval = jstring_to_string(ret, env); Vector types; - String cs = "("; int stringCount = env->GetArrayLength(args); @@ -83,18 +87,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis jstring string = (jstring)env->GetObjectArrayElement(args, i); const String rawString = jstring_to_string(string, env); types.push_back(get_jni_type(rawString)); - cs += get_jni_sig(rawString); - } - - cs += ")"; - cs += get_jni_sig(retval); - jclass cls = env->GetObjectClass(s->get_instance()); - jmethodID mid = env->GetMethodID(cls, mname.ascii().get_data(), cs.ascii().get_data()); - if (!mid) { - print_line("Failed getting method ID " + mname); } - s->add_method(mname, mid, types, get_jni_type(retval)); + s->add_method(mname, types, get_jni_type(retval)); } JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSignal(JNIEnv *env, jclass clazz, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_param_types) { From 5e0805a8138702651cb8cc5fdb8ca5c38bcdc2f0 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Thu, 26 Sep 2024 12:37:02 -0700 Subject: [PATCH 16/30] Provide access to the Android runtime to GDScript Thanks for the fix of `JavaClassWrapper` in https://github.com/godotengine/godot/pull/96182 and the changes in the previous commit, this introduces an `AndroidRuntime` plugin which provides GDScript access to the Android runtime capabilities. This allows developers to get access to various Android capabilities without the need of a plugin. For example, the following logic can be used to check whether the device supports vibration: ``` var android_runtime = Engine.get_singleton("AndroidRuntime") if android_runtime: print("Checking if the device supports vibration") var vibrator_service = android_runtime.getApplicationContext().getSystemService("vibrator") if vibrator_service: if vibrator_service.hasVibrator(): print("Vibration is supported on device!") else: printerr("Vibration is not supported on device") else: printerr("Unable to retrieve the vibrator service") else: printerr("Couldn't find AndroidRuntime singleton") ``` --- .../lib/src/org/godotengine/godot/Godot.kt | 6 +- .../godot/plugin/AndroidRuntimePlugin.kt | 63 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index 5b1d09e7492..567b134234e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -58,6 +58,8 @@ import org.godotengine.godot.input.GodotEditText import org.godotengine.godot.input.GodotInputHandler import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.file.FileAccessHandler +import org.godotengine.godot.plugin.AndroidRuntimePlugin +import org.godotengine.godot.plugin.GodotPlugin import org.godotengine.godot.plugin.GodotPluginRegistry import org.godotengine.godot.tts.GodotTTS import org.godotengine.godot.utils.CommandLineFileParser @@ -228,7 +230,9 @@ class Godot(private val context: Context) { window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) Log.v(TAG, "Initializing Godot plugin registry") - GodotPluginRegistry.initializePluginRegistry(this, primaryHost.getHostPlugins(this)) + val runtimePlugins = mutableSetOf(AndroidRuntimePlugin(this)) + runtimePlugins.addAll(primaryHost.getHostPlugins(this)) + GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins) if (io == null) { io = GodotIO(activity) } diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt b/platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt new file mode 100644 index 00000000000..edb4e7c3578 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt @@ -0,0 +1,63 @@ +/**************************************************************************/ +/* AndroidRuntimePlugin.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.plugin + +import org.godotengine.godot.Godot + +/** + * Provides access to the Android runtime capabilities. + * + * For example, from gdscript, developers can use [getApplicationContext] to access system services + * and check if the device supports vibration. + * + * var android_runtime = Engine.get_singleton("AndroidRuntime") + * if android_runtime: + * print("Checking if the device supports vibration") + * var vibrator_service = android_runtime.getApplicationContext().getSystemService("vibrator") + * if vibrator_service: + * if vibrator_service.hasVibrator(): + * print("Vibration is supported on device!") + * else: + * printerr("Vibration is not supported on device") + * else: + * printerr("Unable to retrieve the vibrator service") + * else: + * printerr("Couldn't find AndroidRuntime singleton") + */ +class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) { + override fun getPluginName() = "AndroidRuntime" + + @UsedByGodot + fun getApplicationContext() = activity?.applicationContext + + @UsedByGodot + override fun getActivity() = super.getActivity() +} From 4587d14796bfd21be3c677efb2273656876f5241 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Thu, 26 Sep 2024 15:12:25 -0700 Subject: [PATCH 17/30] Add logic to automatically pick up jar/aar library dependencies in the `res://addons` directory --- platform/android/export/export_plugin.cpp | 3 +++ platform/android/java/app/build.gradle | 6 ++++++ platform/android/java/app/config.gradle | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index cfd258cddc3..41f460ca8f3 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -3263,8 +3263,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Refglobalize_path("res://addons"); + cmdline.push_back("-p"); // argument to specify the start directory. cmdline.push_back(build_path); // start directory. + cmdline.push_back("-Paddons_directory=" + addons_directory); // path to the addon directory as it may contain jar or aar dependencies cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name. cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code. cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name. diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index fdc57537982..308f126d5d1 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -63,6 +63,12 @@ dependencies { implementation files(pluginsBinaries) } + // Automatically pick up local dependencies in res://addons + String addonsDirectory = getAddonsDirectory() + if (addonsDirectory != null && !addonsDirectory.isBlank()) { + implementation fileTree(dir: "$addonsDirectory", include: ['*.jar', '*.aar']) + } + // .NET dependencies String jar = '../../../../modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar' if (file(jar).exists()) { diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 597a4d5c145..e8921e1bb1f 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -408,3 +408,8 @@ ext.shouldUseLegacyPackaging = { -> // Default behavior for minSdk >= 23 return false } + +ext.getAddonsDirectory = { -> + String addonsDirectory = project.hasProperty("addons_directory") ? project.property("addons_directory") : "" + return addonsDirectory +} From 7a909896f73cf7ed81ba39028b75315ecf851628 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Fri, 13 Sep 2024 13:05:20 -0700 Subject: [PATCH 18/30] Expose `get_export_option_visibility` to editor plugins --- doc/classes/EditorExportPlugin.xml | 9 +++++++++ editor/export/editor_export_plugin.cpp | 7 +++++++ editor/export/editor_export_plugin.h | 2 ++ editor/export/editor_export_preset.cpp | 25 +++++++++++++++++++++++-- 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/doc/classes/EditorExportPlugin.xml b/doc/classes/EditorExportPlugin.xml index 42e1968eb03..aa8e4b3d56c 100644 --- a/doc/classes/EditorExportPlugin.xml +++ b/doc/classes/EditorExportPlugin.xml @@ -159,6 +159,15 @@ Return a [PackedStringArray] of additional features this preset, for the given [param platform], should have. + + + + + + [b]Optional.[/b] + Validates [param option] and returns the visibility for the specified [param platform]. The default implementation returns [code]true[/code] for all options. + + diff --git a/editor/export/editor_export_plugin.cpp b/editor/export/editor_export_plugin.cpp index 5945c024135..999216b4951 100644 --- a/editor/export/editor_export_plugin.cpp +++ b/editor/export/editor_export_plugin.cpp @@ -295,6 +295,12 @@ bool EditorExportPlugin::_should_update_export_options(const Ref &p_export_platform, const String &p_option_name) const { + bool ret = true; + GDVIRTUAL_CALL(_get_export_option_visibility, p_export_platform, p_option_name, ret); + return ret; +} + String EditorExportPlugin::_get_export_option_warning(const Ref &p_export_platform, const String &p_option_name) const { String ret; GDVIRTUAL_CALL(_get_export_option_warning, p_export_platform, p_option_name, ret); @@ -354,6 +360,7 @@ void EditorExportPlugin::_bind_methods() { GDVIRTUAL_BIND(_get_export_options, "platform"); GDVIRTUAL_BIND(_get_export_options_overrides, "platform"); GDVIRTUAL_BIND(_should_update_export_options, "platform"); + GDVIRTUAL_BIND(_get_export_option_visibility, "platform", "option"); GDVIRTUAL_BIND(_get_export_option_warning, "platform", "option"); GDVIRTUAL_BIND(_get_export_features, "platform", "debug"); diff --git a/editor/export/editor_export_plugin.h b/editor/export/editor_export_plugin.h index 4c0107af721..db11d25f7dc 100644 --- a/editor/export/editor_export_plugin.h +++ b/editor/export/editor_export_plugin.h @@ -132,6 +132,7 @@ class EditorExportPlugin : public RefCounted { GDVIRTUAL1RC(TypedArray, _get_export_options, const Ref &); GDVIRTUAL1RC(Dictionary, _get_export_options_overrides, const Ref &); GDVIRTUAL1RC(bool, _should_update_export_options, const Ref &); + GDVIRTUAL2RC(bool, _get_export_option_visibility, const Ref &, String); GDVIRTUAL2RC(String, _get_export_option_warning, const Ref &, String); GDVIRTUAL0RC(String, _get_name) @@ -160,6 +161,7 @@ class EditorExportPlugin : public RefCounted { virtual void _get_export_options(const Ref &p_export_platform, List *r_options) const; virtual Dictionary _get_export_options_overrides(const Ref &p_export_platform) const; virtual bool _should_update_export_options(const Ref &p_export_platform) const; + virtual bool _get_export_option_visibility(const Ref &p_export_platform, const String &p_option_name) const; virtual String _get_export_option_warning(const Ref &p_export_platform, const String &p_option_name) const; public: diff --git a/editor/export/editor_export_preset.cpp b/editor/export/editor_export_preset.cpp index 9f805666d0a..2a468139d45 100644 --- a/editor/export/editor_export_preset.cpp +++ b/editor/export/editor_export_preset.cpp @@ -135,8 +135,29 @@ String EditorExportPreset::_get_property_warning(const StringName &p_name) const void EditorExportPreset::_get_property_list(List *p_list) const { for (const KeyValue &E : properties) { - if (!value_overrides.has(E.key) && platform->get_export_option_visibility(this, E.key)) { - p_list->push_back(E.value); + if (!value_overrides.has(E.key)) { + bool property_visible = platform->get_export_option_visibility(this, E.key); + if (!property_visible) { + continue; + } + + // Get option visibility from editor export plugins. + Vector> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (!export_plugins[i]->supports_platform(platform)) { + continue; + } + + export_plugins.write[i]->set_export_preset(Ref(this)); + property_visible = export_plugins[i]->_get_export_option_visibility(platform, E.key); + if (!property_visible) { + break; + } + } + + if (property_visible) { + p_list->push_back(E.value); + } } } } From e376c4f30c6c220a3b82b7526555b1f3475d4896 Mon Sep 17 00:00:00 2001 From: Pablo Andres Fuente Date: Sat, 28 Sep 2024 00:55:41 -0300 Subject: [PATCH 19/30] Add unit tests for `SceneMultiplayer` This PR aims to help "fix" #43440 Also fixing a small typo on `SceneMultiplayer` docs. --- modules/multiplayer/SCsub | 7 + .../doc_classes/SceneMultiplayer.xml | 2 +- .../tests/test_scene_multiplayer.h | 284 ++++++++++++++++++ 3 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 modules/multiplayer/tests/test_scene_multiplayer.h diff --git a/modules/multiplayer/SCsub b/modules/multiplayer/SCsub index 97f91c56744..f9f4e579e86 100644 --- a/modules/multiplayer/SCsub +++ b/modules/multiplayer/SCsub @@ -13,3 +13,10 @@ if env.editor_build: env_mp.add_source_files(module_obj, "editor/*.cpp") env.modules_sources += module_obj + +if env["tests"]: + env_mp.Append(CPPDEFINES=["TESTS_ENABLED"]) + env_mp.add_source_files(env.modules_sources, "./tests/*.cpp") + + if env["disable_exceptions"]: + env_mp.Append(CPPDEFINES=["DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS"]) diff --git a/modules/multiplayer/doc_classes/SceneMultiplayer.xml b/modules/multiplayer/doc_classes/SceneMultiplayer.xml index 42f32d4848f..3277f1ff3e8 100644 --- a/modules/multiplayer/doc_classes/SceneMultiplayer.xml +++ b/modules/multiplayer/doc_classes/SceneMultiplayer.xml @@ -65,7 +65,7 @@ [b]Warning:[/b] Deserialized objects can contain code which gets executed. Do not use this option if the serialized object comes from untrusted sources to avoid potential security threat such as remote code execution. - The callback to execute when when receiving authentication data sent via [method send_auth]. If the [Callable] is empty (default), peers will be automatically accepted as soon as they connect. + The callback to execute when receiving authentication data sent via [method send_auth]. If the [Callable] is empty (default), peers will be automatically accepted as soon as they connect. If set to a value greater than [code]0.0[/code], the maximum duration in seconds peers can stay in the authenticating state, after which the authentication will automatically fail. See the [signal peer_authenticating] and [signal peer_authentication_failed] signals. diff --git a/modules/multiplayer/tests/test_scene_multiplayer.h b/modules/multiplayer/tests/test_scene_multiplayer.h new file mode 100644 index 00000000000..5e526c9be62 --- /dev/null +++ b/modules/multiplayer/tests/test_scene_multiplayer.h @@ -0,0 +1,284 @@ +/**************************************************************************/ +/* test_scene_multiplayer.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_SCENE_MULTIPLAYER_H +#define TEST_SCENE_MULTIPLAYER_H + +#include "tests/test_macros.h" +#include "tests/test_utils.h" + +#include "../scene_multiplayer.h" + +namespace TestSceneMultiplayer { + +static inline Array build_array() { + return Array(); +} +template +static inline Array build_array(Variant item, Targs... Fargs) { + Array a = build_array(Fargs...); + a.push_front(item); + return a; +} + +TEST_CASE("[Multiplayer][SceneMultiplayer] Defaults") { + Ref scene_multiplayer; + scene_multiplayer.instantiate(); + + REQUIRE(scene_multiplayer->has_multiplayer_peer()); + Ref multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + REQUIRE_MESSAGE(Object::cast_to(multiplayer_peer.ptr()) != nullptr, "By default it must be an OfflineMultiplayerPeer instance."); + CHECK_EQ(scene_multiplayer->poll(), Error::OK); + CHECK_EQ(scene_multiplayer->get_unique_id(), MultiplayerPeer::TARGET_PEER_SERVER); + CHECK_EQ(scene_multiplayer->get_peer_ids(), Vector()); + CHECK_EQ(scene_multiplayer->get_remote_sender_id(), 0); + CHECK_EQ(scene_multiplayer->get_root_path(), NodePath()); + CHECK(scene_multiplayer->get_connected_peers().is_empty()); + CHECK_FALSE(scene_multiplayer->is_refusing_new_connections()); + CHECK_FALSE(scene_multiplayer->is_object_decoding_allowed()); + CHECK(scene_multiplayer->is_server_relay_enabled()); + CHECK_EQ(scene_multiplayer->get_max_sync_packet_size(), 1350); + CHECK_EQ(scene_multiplayer->get_max_delta_packet_size(), 65535); + CHECK(scene_multiplayer->is_server()); +} + +TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] SceneTree has a OfflineMultiplayerPeer by default") { + Ref scene_multiplayer = SceneTree::get_singleton()->get_multiplayer(); + REQUIRE(scene_multiplayer->has_multiplayer_peer()); + + Ref multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + REQUIRE_MESSAGE(Object::cast_to(multiplayer_peer.ptr()) != nullptr, "By default it must be an OfflineMultiplayerPeer instance."); +} + +TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] Object configuration add/remove") { + Ref scene_multiplayer; + scene_multiplayer.instantiate(); + + SUBCASE("Returns invalid parameter") { + CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, "ImInvalid"), Error::ERR_INVALID_PARAMETER); + CHECK_EQ(scene_multiplayer->object_configuration_remove(nullptr, "ImInvalid"), Error::ERR_INVALID_PARAMETER); + + NodePath foo_path("/Foo"); + NodePath bar_path("/Bar"); + CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, foo_path), Error::OK); + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->object_configuration_remove(nullptr, bar_path), Error::ERR_INVALID_PARAMETER); + ERR_PRINT_ON; + } + + SUBCASE("Sets root path") { + NodePath foo_path("/Foo"); + CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, foo_path), Error::OK); + + CHECK_EQ(scene_multiplayer->get_root_path(), foo_path); + } + + SUBCASE("Unsets root path") { + NodePath foo_path("/Foo"); + CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, foo_path), Error::OK); + + CHECK_EQ(scene_multiplayer->object_configuration_remove(nullptr, foo_path), Error::OK); + CHECK_EQ(scene_multiplayer->get_root_path(), NodePath()); + } + + SUBCASE("Add/Remove a MultiplayerSpawner") { + Node2D *node = memnew(Node2D); + MultiplayerSpawner *spawner = memnew(MultiplayerSpawner); + + CHECK_EQ(scene_multiplayer->object_configuration_add(node, spawner), Error::OK); + CHECK_EQ(scene_multiplayer->object_configuration_remove(node, spawner), Error::OK); + + memdelete(spawner); + memdelete(node); + } + + SUBCASE("Add/Remove a MultiplayerSynchronizer") { + Node2D *node = memnew(Node2D); + MultiplayerSynchronizer *synchronizer = memnew(MultiplayerSynchronizer); + + CHECK_EQ(scene_multiplayer->object_configuration_add(node, synchronizer), Error::OK); + CHECK_EQ(scene_multiplayer->object_configuration_remove(node, synchronizer), Error::OK); + + memdelete(synchronizer); + memdelete(node); + } +} + +TEST_CASE("[Multiplayer][SceneMultiplayer] Root Path") { + Ref scene_multiplayer; + scene_multiplayer.instantiate(); + + SUBCASE("Is set") { + NodePath foo_path("/Foo"); + scene_multiplayer->set_root_path(foo_path); + + CHECK_EQ(scene_multiplayer->get_root_path(), foo_path); + } + + SUBCASE("Fails when path is empty") { + ERR_PRINT_OFF; + scene_multiplayer->set_root_path(NodePath()); + ERR_PRINT_ON; + } + + SUBCASE("Fails when path is relative") { + NodePath foo_path("Foo"); + ERR_PRINT_OFF; + scene_multiplayer->set_root_path(foo_path); + ERR_PRINT_ON; + + CHECK_EQ(scene_multiplayer->get_root_path(), NodePath()); + } +} + +// This one could be a dummy callback because the current set of test is not actually testing the full auth flow. +static Variant auth_callback(Variant sv, Variant pvav) { + return Variant(); +} + +TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] Send Authentication") { + Ref scene_multiplayer; + scene_multiplayer.instantiate(); + SceneTree::get_singleton()->set_multiplayer(scene_multiplayer); + scene_multiplayer->set_auth_callback(callable_mp_static(auth_callback)); + + SUBCASE("Is properly sent") { + SIGNAL_WATCH(scene_multiplayer.ptr(), "peer_authenticating"); + + // Adding a peer to MultiplayerPeer. + Ref multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int peer_id = 42; + multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id); + SIGNAL_CHECK("peer_authenticating", build_array(build_array(peer_id))); + + CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::OK); + + Vector expected_peer_ids = { peer_id }; + CHECK_EQ(scene_multiplayer->get_authenticating_peer_ids(), expected_peer_ids); + + SIGNAL_UNWATCH(scene_multiplayer.ptr(), "peer_authenticating"); + } + + SUBCASE("peer_authentication_failed is emitted when a peer is deleted before authentication is completed") { + SIGNAL_WATCH(scene_multiplayer.ptr(), "peer_authentication_failed"); + + // Adding a peer to MultiplayerPeer. + Ref multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int peer_id = 42; + multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id); + multiplayer_peer->emit_signal(SNAME("peer_disconnected"), peer_id); + SIGNAL_CHECK("peer_authentication_failed", build_array(build_array(peer_id))); + + SIGNAL_UNWATCH(scene_multiplayer.ptr(), "peer_authentication_failed"); + } + + SUBCASE("peer_authentication_failed is emitted when authentication timeout") { + SIGNAL_WATCH(scene_multiplayer.ptr(), "peer_authentication_failed"); + scene_multiplayer->set_auth_timeout(0.01); + CHECK_EQ(scene_multiplayer->get_auth_timeout(), 0.01); + + // Adding two peesr to MultiplayerPeer. + Ref multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int first_peer_id = 42; + int second_peer_id = 84; + multiplayer_peer->emit_signal(SNAME("peer_connected"), first_peer_id); + multiplayer_peer->emit_signal(SNAME("peer_connected"), second_peer_id); + + // Let timeout happens. + OS::get_singleton()->delay_usec(500000); + + CHECK_EQ(scene_multiplayer->poll(), Error::OK); + + SIGNAL_CHECK("peer_authentication_failed", build_array(build_array(first_peer_id), build_array(second_peer_id))); + + SIGNAL_UNWATCH(scene_multiplayer.ptr(), "peer_authentication_failed"); + } + + SUBCASE("Fails when there is no MultiplayerPeer configured") { + scene_multiplayer->set_multiplayer_peer(nullptr); + + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->send_auth(42, Vector()), Error::ERR_UNCONFIGURED); + ERR_PRINT_ON; + } + + SUBCASE("Fails when the peer to send the auth is not pending") { + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->send_auth(42, String("It's me").to_ascii_buffer()), Error::ERR_INVALID_PARAMETER); + ERR_PRINT_ON; + } +} + +TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] Complete Authentication") { + Ref scene_multiplayer; + scene_multiplayer.instantiate(); + SceneTree::get_singleton()->set_multiplayer(scene_multiplayer); + scene_multiplayer->set_auth_callback(callable_mp_static(auth_callback)); + + SUBCASE("Is properly completed") { + Ref multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int peer_id = 42; + multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id); + CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::OK); + + CHECK_EQ(scene_multiplayer->complete_auth(peer_id), Error::OK); + } + + SUBCASE("Fails when there is no MultiplayerPeer configured") { + scene_multiplayer->set_multiplayer_peer(nullptr); + + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->complete_auth(42), Error::ERR_UNCONFIGURED); + ERR_PRINT_ON; + } + + SUBCASE("Fails when the peer to complete the auth is not pending") { + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->complete_auth(42), Error::ERR_INVALID_PARAMETER); + ERR_PRINT_ON; + } + + SUBCASE("Fails to send auth or completed for a second time") { + Ref multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int peer_id = 42; + multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id); + CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::OK); + CHECK_EQ(scene_multiplayer->complete_auth(peer_id), Error::OK); + + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::ERR_FILE_CANT_WRITE); + CHECK_EQ(scene_multiplayer->complete_auth(peer_id), Error::ERR_FILE_CANT_WRITE); + ERR_PRINT_ON; + } +} + +} // namespace TestSceneMultiplayer + +#endif // TEST_SCENE_MULTIPLAYER_H From 03c3c5f608c9919625b8a39fdf24e8250600b127 Mon Sep 17 00:00:00 2001 From: HolonProduction Date: Sat, 12 Aug 2023 22:12:28 +0200 Subject: [PATCH 20/30] Set position to zero when saving a positioned branch as scene --- editor/scene_tree_dock.cpp | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 7187da851ec..ad84846a6df 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -436,6 +436,22 @@ void SceneTreeDock::_replace_with_branch_scene(const String &p_file, Node *base) instantiated_scene->set_unique_name_in_owner(base->is_unique_name_in_owner()); + Node2D *copy_2d = Object::cast_to(instantiated_scene); + Node2D *base_2d = Object::cast_to(base); + if (copy_2d && base_2d) { + copy_2d->set_position(base_2d->get_position()); + copy_2d->set_rotation(base_2d->get_rotation()); + copy_2d->set_scale(base_2d->get_scale()); + } + + Node3D *copy_3d = Object::cast_to(instantiated_scene); + Node3D *base_3d = Object::cast_to(base); + if (copy_3d && base_3d) { + copy_3d->set_position(base_3d->get_position()); + copy_3d->set_rotation(base_3d->get_rotation()); + copy_3d->set_scale(base_3d->get_scale()); + } + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Replace with Branch Scene")); @@ -3259,6 +3275,36 @@ void SceneTreeDock::_new_scene_from(const String &p_file) { // Root node cannot ever be unique name in its own Scene! copy->set_unique_name_in_owner(false); + const Dictionary dict = new_scene_from_dialog->get_selected_options(); + bool reset_position = dict.get(TTR("Reset Position"), true); + bool reset_scale = dict.get(TTR("Reset Scale"), false); + bool reset_rotation = dict.get(TTR("Reset Rotation"), false); + + Node2D *copy_2d = Object::cast_to(copy); + if (copy_2d != nullptr) { + if (reset_position) { + copy_2d->set_position(Vector2(0, 0)); + } + if (reset_rotation) { + copy_2d->set_rotation(0); + } + if (reset_scale) { + copy_2d->set_scale(Size2(1, 1)); + } + } + Node3D *copy_3d = Object::cast_to(copy); + if (copy_3d != nullptr) { + if (reset_position) { + copy_3d->set_position(Vector3(0, 0, 0)); + } + if (reset_rotation) { + copy_3d->set_rotation(Vector3(0, 0, 0)); + } + if (reset_scale) { + copy_3d->set_scale(Vector3(0, 0, 0)); + } + } + Ref sdata = memnew(PackedScene); Error err = sdata->pack(copy); memdelete(copy); @@ -4668,6 +4714,9 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec new_scene_from_dialog = memnew(EditorFileDialog); new_scene_from_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); + new_scene_from_dialog->add_option(TTR("Reset Position"), Vector(), true); + new_scene_from_dialog->add_option(TTR("Reset Rotation"), Vector(), false); + new_scene_from_dialog->add_option(TTR("Reset Scale"), Vector(), false); add_child(new_scene_from_dialog); new_scene_from_dialog->connect("file_selected", callable_mp(this, &SceneTreeDock::_new_scene_from)); From a6997d37cc9da98e1e876178f3e5e657d8fb6ba6 Mon Sep 17 00:00:00 2001 From: Max Hilbrunner Date: Mon, 30 Sep 2024 12:21:30 +0200 Subject: [PATCH 21/30] Docs: remove duplicate words --- doc/classes/@GlobalScope.xml | 2 +- doc/classes/CameraAttributesPhysical.xml | 2 +- doc/classes/PopupMenu.xml | 2 +- doc/classes/PopupPanel.xml | 2 +- doc/classes/ProjectSettings.xml | 2 +- doc/classes/RenderingServer.xml | 2 +- doc/classes/Vector4i.xml | 2 +- modules/multiplayer/doc_classes/SceneMultiplayer.xml | 2 +- modules/webxr/doc_classes/WebXRInterface.xml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 63d20242d62..55d00b6cf90 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -1209,7 +1209,7 @@ - Returns [code]-1[/code] if [param x] is negative, [code]1[/code] if [param x] is positive, and [code]0[/code] if if [param x] is zero. + Returns [code]-1[/code] if [param x] is negative, [code]1[/code] if [param x] is positive, and [code]0[/code] if [param x] is zero. [codeblock] signi(-6) # Returns -1 signi(0) # Returns 0 diff --git a/doc/classes/CameraAttributesPhysical.xml b/doc/classes/CameraAttributesPhysical.xml index faedfee7121..e2036162c79 100644 --- a/doc/classes/CameraAttributesPhysical.xml +++ b/doc/classes/CameraAttributesPhysical.xml @@ -25,7 +25,7 @@ The maximum luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing below a certain brightness, resulting in a cut off point where the scene will remain bright. - The minimum luminance luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing above a certain brightness, resulting in a cut off point where the scene will remain dark. + The minimum luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing above a certain brightness, resulting in a cut off point where the scene will remain dark. Size of the aperture of the camera, measured in f-stops. An f-stop is a unitless ratio between the focal length of the camera and the diameter of the aperture. A high aperture setting will result in a smaller aperture which leads to a dimmer image and sharper focus. A low aperture results in a wide aperture which lets in more light resulting in a brighter, less-focused image. Default is appropriate for outdoors at daytime (i.e. for use with a default [DirectionalLight3D]), for indoor lighting, a value between 2 and 4 is more appropriate. diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml index 004bbe2286b..d73cda74602 100644 --- a/doc/classes/PopupMenu.xml +++ b/doc/classes/PopupMenu.xml @@ -776,7 +776,7 @@ [StyleBox] for the right side of labeled separator. See [method add_separator]. - [StyleBox] for the the background panel. + [StyleBox] for the background panel. [StyleBox] used for the separators. See [method add_separator]. diff --git a/doc/classes/PopupPanel.xml b/doc/classes/PopupPanel.xml index 399e2854025..b581f32686c 100644 --- a/doc/classes/PopupPanel.xml +++ b/doc/classes/PopupPanel.xml @@ -10,7 +10,7 @@ - [StyleBox] for the the background panel. + [StyleBox] for the background panel. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index af5ec41b60b..4266bab2a17 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1369,7 +1369,7 @@ macOS specific override for the shortcut to select the word currently under the caret. - If no selection is currently active with the last caret in text fields, searches for the next occurrence of the the word currently under the caret and moves the caret to the next occurrence. The action can be performed sequentially for other occurrences of the word under the last caret. + If no selection is currently active with the last caret in text fields, searches for the next occurrence of the word currently under the caret and moves the caret to the next occurrence. The action can be performed sequentially for other occurrences of the word under the last caret. If a selection is currently active with the last caret in text fields, searches for the next occurrence of the selection, adds a caret, selects the next occurrence then deselects the previous selection and its associated caret. The action can be performed sequentially for other occurrences of the selection of the last caret. The viewport is adjusted to the latest newly added caret. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index a57f6adec8d..91af70b565f 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -913,7 +913,7 @@ Transforms both the current and previous stored transform for a canvas light. - This allows transforming a light without creating a "glitch" in the interpolation, which is is particularly useful for large worlds utilizing a shifting origin. + This allows transforming a light without creating a "glitch" in the interpolation, which is particularly useful for large worlds utilizing a shifting origin. diff --git a/doc/classes/Vector4i.xml b/doc/classes/Vector4i.xml index 8f54b767e00..b351f2ccb61 100644 --- a/doc/classes/Vector4i.xml +++ b/doc/classes/Vector4i.xml @@ -216,7 +216,7 @@ - Gets the remainder of each component of the [Vector4i] with the the given [int]. This operation uses truncated division, which is often not desired as it does not work well with negative numbers. Consider using [method @GlobalScope.posmod] instead if you want to handle negative numbers. + Gets the remainder of each component of the [Vector4i] with the given [int]. This operation uses truncated division, which is often not desired as it does not work well with negative numbers. Consider using [method @GlobalScope.posmod] instead if you want to handle negative numbers. [codeblock] print(Vector4i(10, -20, 30, -40) % 7) # Prints "(3, -6, 2, -5)" [/codeblock] diff --git a/modules/multiplayer/doc_classes/SceneMultiplayer.xml b/modules/multiplayer/doc_classes/SceneMultiplayer.xml index 42f32d4848f..3277f1ff3e8 100644 --- a/modules/multiplayer/doc_classes/SceneMultiplayer.xml +++ b/modules/multiplayer/doc_classes/SceneMultiplayer.xml @@ -65,7 +65,7 @@ [b]Warning:[/b] Deserialized objects can contain code which gets executed. Do not use this option if the serialized object comes from untrusted sources to avoid potential security threat such as remote code execution. - The callback to execute when when receiving authentication data sent via [method send_auth]. If the [Callable] is empty (default), peers will be automatically accepted as soon as they connect. + The callback to execute when receiving authentication data sent via [method send_auth]. If the [Callable] is empty (default), peers will be automatically accepted as soon as they connect. If set to a value greater than [code]0.0[/code], the maximum duration in seconds peers can stay in the authenticating state, after which the authentication will automatically fail. See the [signal peer_authenticating] and [signal peer_authentication_failed] signals. diff --git a/modules/webxr/doc_classes/WebXRInterface.xml b/modules/webxr/doc_classes/WebXRInterface.xml index bd7192520ac..829279e7bb1 100644 --- a/modules/webxr/doc_classes/WebXRInterface.xml +++ b/modules/webxr/doc_classes/WebXRInterface.xml @@ -283,7 +283,7 @@ - We don't know the the target ray mode. + We don't know the target ray mode. Target ray originates at the viewer's eyes and points in the direction they are looking. From c4b7d6f5c32e2a241666233996f20af68107d0ba Mon Sep 17 00:00:00 2001 From: Travis Wrightsman Date: Mon, 30 Sep 2024 09:03:15 -0700 Subject: [PATCH 22/30] Fix ICU support data loading This fixes an error in loading the ICU support data on platforms that don't use either the builtin icu4c or the static ICU support data. --- modules/text_server_adv/text_server_adv.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 3322300ddaf..e5639744d39 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -442,6 +442,8 @@ bool TextServerAdvanced::_load_support_data(const String &p_filename) { } #else if (!icu_data_loaded) { + UErrorCode err = U_ZERO_ERROR; +#ifdef ICU_DATA_NAME String filename = (p_filename.is_empty()) ? String("res://") + _MKSTR(ICU_DATA_NAME) : p_filename; Ref f = FileAccess::open(filename, FileAccess::READ); @@ -451,13 +453,13 @@ bool TextServerAdvanced::_load_support_data(const String &p_filename) { uint64_t len = f->get_length(); icu_data = f->get_buffer(len); - UErrorCode err = U_ZERO_ERROR; udata_setCommonData(icu_data.ptr(), &err); if (U_FAILURE(err)) { ERR_FAIL_V_MSG(false, u_errorName(err)); } err = U_ZERO_ERROR; +#endif u_init(&err); if (U_FAILURE(err)) { ERR_FAIL_V_MSG(false, u_errorName(err)); From 3c365a7fa5de8778488adefcb296ab42fe29ac15 Mon Sep 17 00:00:00 2001 From: Haoyu Qiu Date: Mon, 23 Sep 2024 15:44:12 +0800 Subject: [PATCH 23/30] Add auto translate mode for cells in Tree --- doc/classes/TreeItem.xml | 16 +++++++++++++ scene/gui/tree.cpp | 49 ++++++++++++++++++++++++++++++++++++---- scene/gui/tree.h | 7 ++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/doc/classes/TreeItem.xml b/doc/classes/TreeItem.xml index 861a53aaad1..132ecc3f926 100644 --- a/doc/classes/TreeItem.xml +++ b/doc/classes/TreeItem.xml @@ -73,6 +73,13 @@ Removes the button at index [param button_index] in column [param column]. + + + + + Returns the column's auto translate mode. + + @@ -493,6 +500,15 @@ Selects the given [param column]. + + + + + + Sets the given column's auto translate mode to [param mode]. + All columns use [constant Node.AUTO_TRANSLATE_MODE_INHERIT] by default, which uses the same auto translate mode as the [Tree] itself. + + diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index e4f52ee8ee0..8238d54381b 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -185,6 +185,26 @@ TreeItem::TreeCellMode TreeItem::get_cell_mode(int p_column) const { return cells[p_column].mode; } +/* auto translate mode */ +void TreeItem::set_auto_translate_mode(int p_column, Node::AutoTranslateMode p_mode) { + ERR_FAIL_INDEX(p_column, cells.size()); + + if (cells[p_column].auto_translate_mode == p_mode) { + return; + } + + cells.write[p_column].auto_translate_mode = p_mode; + cells.write[p_column].dirty = true; + cells.write[p_column].cached_minimum_size_dirty = true; + + _changed_notify(p_column); +} + +Node::AutoTranslateMode TreeItem::get_auto_translate_mode(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), Node::AUTO_TRANSLATE_MODE_INHERIT); + return cells[p_column].auto_translate_mode; +} + /* multiline editable */ void TreeItem::set_edit_multiline(int p_column, bool p_multiline) { ERR_FAIL_INDEX(p_column, cells.size()); @@ -247,6 +267,24 @@ void TreeItem::propagate_check(int p_column, bool p_emit_signal) { _propagate_check_through_parents(p_column, p_emit_signal); } +String TreeItem::atr(int p_column, const String &p_text) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), tree->atr(p_text)); + + switch (cells[p_column].auto_translate_mode) { + case Node::AUTO_TRANSLATE_MODE_INHERIT: { + return tree->atr(p_text); + } break; + case Node::AUTO_TRANSLATE_MODE_ALWAYS: { + return tree->tr(p_text); + } break; + case Node::AUTO_TRANSLATE_MODE_DISABLED: { + return p_text; + } break; + } + + ERR_FAIL_V_MSG(tree->atr(p_text), "Unexpected auto translate mode: " + itos(cells[p_column].auto_translate_mode)); +} + void TreeItem::_propagate_check_through_children(int p_column, bool p_checked, bool p_emit_signal) { TreeItem *current = get_first_child(); while (current) { @@ -323,7 +361,7 @@ void TreeItem::set_text(int p_column, String p_text) { } else { // Don't auto translate if it's in string mode and editable, as the text can be changed to anything by the user. if (tree && (!cells[p_column].editable || cells[p_column].mode != TreeItem::CELL_MODE_STRING)) { - cells.write[p_column].xl_text = tree->atr(p_text); + cells.write[p_column].xl_text = atr(p_column, p_text); } else { cells.write[p_column].xl_text = p_text; } @@ -1621,6 +1659,9 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_cell_mode", "column", "mode"), &TreeItem::set_cell_mode); ClassDB::bind_method(D_METHOD("get_cell_mode", "column"), &TreeItem::get_cell_mode); + ClassDB::bind_method(D_METHOD("set_auto_translate_mode", "column", "mode"), &TreeItem::set_auto_translate_mode); + ClassDB::bind_method(D_METHOD("get_auto_translate_mode", "column"), &TreeItem::get_auto_translate_mode); + ClassDB::bind_method(D_METHOD("set_edit_multiline", "column", "multiline"), &TreeItem::set_edit_multiline); ClassDB::bind_method(D_METHOD("is_edit_multiline", "column"), &TreeItem::is_edit_multiline); @@ -2009,7 +2050,7 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) { int option = (int)p_item->cells[p_col].val; - valtext = atr(ETR("(Other)")); + valtext = p_item->atr(p_col, ETR("(Other)")); Vector strings = p_item->cells[p_col].text.split(","); for (int j = 0; j < strings.size(); j++) { int value = j; @@ -2017,7 +2058,7 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) { value = strings[j].get_slicec(':', 1).to_int(); } if (option == value) { - valtext = atr(strings[j].get_slicec(':', 0)); + valtext = p_item->atr(p_col, strings[j].get_slicec(':', 0)); break; } } @@ -2028,7 +2069,7 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) { } else { // Don't auto translate if it's in string mode and editable, as the text can be changed to anything by the user. if (!p_item->cells[p_col].editable || p_item->cells[p_col].mode != TreeItem::CELL_MODE_STRING) { - p_item->cells.write[p_col].xl_text = atr(p_item->cells[p_col].text); + p_item->cells.write[p_col].xl_text = p_item->atr(p_col, p_item->cells[p_col].text); } else { p_item->cells.write[p_col].xl_text = p_item->cells[p_col].text; } diff --git a/scene/gui/tree.h b/scene/gui/tree.h index 17ea31a733f..86efdfec522 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -64,6 +64,7 @@ class TreeItem : public Object { Rect2i icon_region; String text; String xl_text; + Node::AutoTranslateMode auto_translate_mode = Node::AUTO_TRANSLATE_MODE_INHERIT; bool edit_multiline = false; String suffix; Ref text_buf; @@ -210,6 +211,10 @@ class TreeItem : public Object { void set_cell_mode(int p_column, TreeCellMode p_mode); TreeCellMode get_cell_mode(int p_column) const; + /* auto translate mode */ + void set_auto_translate_mode(int p_column, Node::AutoTranslateMode p_mode); + Node::AutoTranslateMode get_auto_translate_mode(int p_column) const; + /* multiline editable */ void set_edit_multiline(int p_column, bool p_multiline); bool is_edit_multiline(int p_column) const; @@ -222,6 +227,8 @@ class TreeItem : public Object { void propagate_check(int p_column, bool p_emit_signal = true); + String atr(int p_column, const String &p_text) const; + private: // Check helpers. void _propagate_check_through_children(int p_column, bool p_checked, bool p_emit_signal); From 1a2e0b22b6278cef95a528e9101a53f4cb93b548 Mon Sep 17 00:00:00 2001 From: volkov Date: Tue, 1 Oct 2024 13:59:48 +0300 Subject: [PATCH 24/30] Add Ukrainian translation for Linux .desktop file --- misc/dist/linux/org.godotengine.Godot.desktop | 2 ++ 1 file changed, 2 insertions(+) diff --git a/misc/dist/linux/org.godotengine.Godot.desktop b/misc/dist/linux/org.godotengine.Godot.desktop index 28669548d6c..6483f51d920 100644 --- a/misc/dist/linux/org.godotengine.Godot.desktop +++ b/misc/dist/linux/org.godotengine.Godot.desktop @@ -5,12 +5,14 @@ GenericName[el]=Ελεύθερη μηχανή παιχνιδιού GenericName[fr]=Moteur de jeu libre GenericName[nl]=Libre game-engine GenericName[ru]=Свободный игровой движок +GenericName[uk]=Вільний ігровий рушій GenericName[zh_CN]=自由的游戏引擎 Comment=Multi-platform 2D and 3D game engine with a feature-rich editor Comment[el]=2D και 3D μηχανή παιχνιδιού πολλαπλών πλατφορμών με επεξεργαστή πλούσιο σε χαρακτηριστικά Comment[fr]=Moteur de jeu 2D et 3D multiplateforme avec un éditeur riche en fonctionnalités Comment[nl]=Multi-platform 2D- en 3D-game-engine met een veelzijdige editor Comment[ru]=Кроссплатформенный движок с многофункциональным редактором для 2D- и 3D-игр +Comment[uk]=Багатофункціональний кросплатформний рушій для створення 2D та 3D ігор Comment[zh_CN]=多平台 2D 和 3D 游戏引擎,带有功能丰富的编辑器 Exec=godot %f Icon=godot From 645abdbb801c29ba93adefb01e422dc66fb22fc2 Mon Sep 17 00:00:00 2001 From: kobewi Date: Mon, 30 Sep 2024 17:48:27 +0200 Subject: [PATCH 25/30] Add expression evaluater to debugger (REPL) Co-authored-by: rohanrhu --- core/debugger/remote_debugger.cpp | 36 +++++ .../debug_adapter/debug_adapter_protocol.cpp | 2 +- editor/debugger/editor_debugger_inspector.cpp | 15 +- editor/debugger/editor_debugger_inspector.h | 2 +- .../debugger/editor_expression_evaluator.cpp | 148 ++++++++++++++++++ editor/debugger/editor_expression_evaluator.h | 77 +++++++++ editor/debugger/script_editor_debugger.cpp | 15 +- editor/debugger/script_editor_debugger.h | 2 + 8 files changed, 291 insertions(+), 6 deletions(-) create mode 100644 editor/debugger/editor_expression_evaluator.cpp create mode 100644 editor/debugger/editor_expression_evaluator.h diff --git a/core/debugger/remote_debugger.cpp b/core/debugger/remote_debugger.cpp index e2ed7245a24..fc1b7b74f99 100644 --- a/core/debugger/remote_debugger.cpp +++ b/core/debugger/remote_debugger.cpp @@ -37,6 +37,7 @@ #include "core/debugger/script_debugger.h" #include "core/input/input.h" #include "core/io/resource_loader.h" +#include "core/math/expression.h" #include "core/object/script_language.h" #include "core/os/os.h" #include "servers/display_server.h" @@ -529,6 +530,41 @@ void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { } else if (command == "set_skip_breakpoints") { ERR_FAIL_COND(data.is_empty()); script_debugger->set_skip_breakpoints(data[0]); + } else if (command == "evaluate") { + String expression_str = data[0]; + int frame = data[1]; + + ScriptInstance *breaked_instance = script_debugger->get_break_language()->debug_get_stack_level_instance(frame); + if (!breaked_instance) { + break; + } + + List locals; + List local_vals; + + script_debugger->get_break_language()->debug_get_stack_level_locals(frame, &locals, &local_vals); + ERR_FAIL_COND(locals.size() != local_vals.size()); + + PackedStringArray locals_vector; + for (const String &S : locals) { + locals_vector.append(S); + } + + Array local_vals_array; + for (const Variant &V : local_vals) { + local_vals_array.append(V); + } + + Expression expression; + expression.parse(expression_str, locals_vector); + const Variant return_val = expression.execute(local_vals_array, breaked_instance->get_owner()); + + DebuggerMarshalls::ScriptStackVariable stvar; + stvar.name = expression_str; + stvar.value = return_val; + stvar.type = 3; + + send_message("evaluation_return", stvar.serialize()); } else { bool captured = false; ERR_CONTINUE(_try_capture(command, data, captured) != OK); diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp index 4febb8bf047..f847d3be7b1 100644 --- a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp @@ -966,7 +966,7 @@ void DebugAdapterProtocol::on_debug_stack_frame_var(const Array &p_data) { List scope_ids = stackframe_list.find(frame)->value; ERR_FAIL_COND(scope_ids.size() != 3); - ERR_FAIL_INDEX(stack_var.type, 3); + ERR_FAIL_INDEX(stack_var.type, 4); int var_id = scope_ids.get(stack_var.type); DAP::Variable variable; diff --git a/editor/debugger/editor_debugger_inspector.cpp b/editor/debugger/editor_debugger_inspector.cpp index cb5e4375a64..e085e2e4480 100644 --- a/editor/debugger/editor_debugger_inspector.cpp +++ b/editor/debugger/editor_debugger_inspector.cpp @@ -223,7 +223,7 @@ Object *EditorDebuggerInspector::get_object(ObjectID p_id) { return nullptr; } -void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { +void EditorDebuggerInspector::add_stack_variable(const Array &p_array, int p_offset) { DebuggerMarshalls::ScriptStackVariable var; var.deserialize(p_array); String n = var.name; @@ -248,6 +248,9 @@ void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { case 2: type = "Globals/"; break; + case 3: + type = "Evaluated/"; + break; default: type = "Unknown/"; } @@ -258,7 +261,15 @@ void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { pinfo.hint = h; pinfo.hint_string = hs; - variables->prop_list.push_back(pinfo); + if ((p_offset == -1) || variables->prop_list.is_empty()) { + variables->prop_list.push_back(pinfo); + } else { + List::Element *current = variables->prop_list.front(); + for (int i = 0; i < p_offset; i++) { + current = current->next(); + } + variables->prop_list.insert_before(current, pinfo); + } variables->prop_values[type + n] = v; variables->update(); edit(variables); diff --git a/editor/debugger/editor_debugger_inspector.h b/editor/debugger/editor_debugger_inspector.h index 73dd773750d..fac95259436 100644 --- a/editor/debugger/editor_debugger_inspector.h +++ b/editor/debugger/editor_debugger_inspector.h @@ -90,7 +90,7 @@ class EditorDebuggerInspector : public EditorInspector { // Stack Dump variables String get_stack_variable(const String &p_var); - void add_stack_variable(const Array &p_arr); + void add_stack_variable(const Array &p_arr, int p_offset = -1); void clear_stack_variables(); }; diff --git a/editor/debugger/editor_expression_evaluator.cpp b/editor/debugger/editor_expression_evaluator.cpp new file mode 100644 index 00000000000..e8b1e33d205 --- /dev/null +++ b/editor/debugger/editor_expression_evaluator.cpp @@ -0,0 +1,148 @@ +/**************************************************************************/ +/* editor_expression_evaluator.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_expression_evaluator.h" + +#include "editor/debugger/editor_debugger_inspector.h" +#include "editor/debugger/script_editor_debugger.h" +#include "scene/gui/button.h" +#include "scene/gui/check_box.h" + +void EditorExpressionEvaluator::on_start() { + expression_input->set_editable(false); + evaluate_btn->set_disabled(true); + + if (clear_on_run_checkbox->is_pressed()) { + inspector->clear_stack_variables(); + } +} + +void EditorExpressionEvaluator::set_editor_debugger(ScriptEditorDebugger *p_editor_debugger) { + editor_debugger = p_editor_debugger; +} + +void EditorExpressionEvaluator::add_value(const Array &p_array) { + inspector->add_stack_variable(p_array, 0); + inspector->set_v_scroll(0); + inspector->set_h_scroll(0); +} + +void EditorExpressionEvaluator::_evaluate() { + const String &expression = expression_input->get_text(); + if (expression.is_empty()) { + return; + } + + if (!editor_debugger->is_session_active()) { + return; + } + + Array expr_data; + expr_data.push_back(expression); + expr_data.push_back(editor_debugger->get_stack_script_frame()); + editor_debugger->send_message("evaluate", expr_data); + + expression_input->clear(); +} + +void EditorExpressionEvaluator::_clear() { + inspector->clear_stack_variables(); +} + +void EditorExpressionEvaluator::_remote_object_selected(ObjectID p_id) { + editor_debugger->emit_signal(SNAME("remote_object_requested"), p_id); +} + +void EditorExpressionEvaluator::_on_expression_input_changed(const String &p_expression) { + evaluate_btn->set_disabled(p_expression.is_empty()); +} + +void EditorExpressionEvaluator::_on_debugger_breaked(bool p_breaked, bool p_can_debug) { + expression_input->set_editable(p_breaked); + evaluate_btn->set_disabled(!p_breaked); +} + +void EditorExpressionEvaluator::_on_debugger_clear_execution(Ref