diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml
index b7591ed4f43b..ea856e757ddf 100644
--- a/doc/classes/Node.xml
+++ b/doc/classes/Node.xml
@@ -880,6 +880,9 @@
Notification received when the node is enabled again after being disabled. See [constant PROCESS_MODE_DISABLED].
+
+ Notification received when other nodes in the tree may have been removed/replaced and node pointers may require re-caching.
+
Notification received right before the scene with the node is saved in the editor. This notification is only sent in the Godot editor and will not occur in exported projects.
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 7697bbfdf40c..5247f493eab8 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -61,6 +61,7 @@
#include "scene/gui/tab_container.h"
#include "scene/gui/texture_progress_bar.h"
#include "scene/main/window.h"
+#include "scene/property_utils.h"
#include "scene/resources/packed_scene.h"
#include "servers/display_server.h"
#include "servers/navigation_server_2d.h"
@@ -1020,6 +1021,10 @@ void EditorNode::_resources_reimported(const Vector &p_resources) {
for (const String &E : scenes) {
reload_scene(E);
+
+ if (EditorSettings::get_singleton()->get("interface/editor/update_reimported_scenes")) {
+ reload_instances_with_path_in_edited_scenes(E);
+ }
}
scene_tabs->set_current_tab(current_tab);
@@ -3767,6 +3772,206 @@ Error EditorNode::load_scene(const String &p_scene, bool p_ignore_broken_deps, b
return OK;
}
+Transform3D EditorNode::get_node_3d_relative_transform_without_tree(const Node3D *p_node, const Node *p_root) const {
+ if (p_node == p_root) {
+ return Transform3D();
+ }
+
+ Transform3D gt;
+ const Node3D *parent = Object::cast_to(p_node->get_parent());
+ if (p_node->get_parent() && !p_node->is_set_as_top_level()) {
+ gt = get_node_3d_relative_transform_without_tree(parent, p_root) * p_node->get_transform();
+ } else {
+ gt = p_node->get_transform();
+ }
+
+ if (p_node->is_scale_disabled()) {
+ gt.basis = gt.basis.orthonormalized();
+ }
+
+ return gt;
+}
+
+Transform2D EditorNode::get_canvas_item_relative_transform_without_tree(const CanvasItem *p_node, const Node *p_root) const {
+ if (p_node == p_root) {
+ return Transform2D();
+ }
+
+ Transform2D gt;
+ const CanvasItem *parent = Object::cast_to(p_node->get_parent());
+ if (parent) {
+ gt = get_canvas_item_relative_transform_without_tree(parent, p_root) * p_node->get_transform();
+ } else {
+ gt = p_node->get_transform();
+ }
+
+ return gt;
+}
+
+void EditorNode::set_node_3d_relative_transform_without_tree(Node3D *p_node, const Node *p_root, const Transform3D p_transform) const {
+ Node3D *parent = Object::cast_to(p_node->get_parent());
+
+ if (parent && !p_node->is_set_as_top_level()) {
+ p_node->set_transform(get_node_3d_relative_transform_without_tree(parent, p_root).affine_inverse() * p_transform);
+ } else {
+ p_node->set_transform(p_transform);
+ }
+}
+
+void EditorNode::set_node_2d_relative_transform_without_tree(Node2D *p_node, const Node *p_root, const Transform2D p_transform) const {
+ CanvasItem *parent = Object::cast_to(p_node->get_parent());
+ if (parent) {
+ p_node->set_transform(get_canvas_item_relative_transform_without_tree(parent, p_root).affine_inverse() * p_transform);
+ } else {
+ p_node->set_transform(p_transform);
+ }
+}
+
+HashMap EditorNode::get_modified_properties_for_node(Node *p_node) {
+ HashMap modified_property_map;
+
+ List pinfo;
+ p_node->get_property_list(&pinfo);
+ for (const PropertyInfo &E : pinfo) {
+ if (E.usage & PROPERTY_USAGE_STORAGE) {
+ bool is_valid_revert = false;
+ Variant revert_value = EditorPropertyRevert::get_property_revert_value(p_node, E.name, &is_valid_revert);
+ Variant current_value = p_node->get(E.name);
+ if (is_valid_revert) {
+ if (PropertyUtils::is_property_value_different(current_value, revert_value)) {
+ modified_property_map[E.name] = current_value;
+ }
+ }
+ }
+ }
+
+ return modified_property_map;
+}
+
+void EditorNode::update_diff_data_for_node(
+ Node *p_edited_scene,
+ Node *p_root,
+ Node *p_node,
+ HashMap &p_modification_table,
+ List &p_addition_list) {
+ bool node_part_of_subscene = p_node != p_edited_scene &&
+ p_edited_scene->get_scene_inherited_state().is_valid() &&
+ p_edited_scene->get_scene_inherited_state()->find_node_by_path(p_edited_scene->get_path_to(p_node)) >= 0;
+
+ // Loop through the owners until either we reach the root node or nullptr
+ Node *valid_node_owner = p_node->get_owner();
+ while (valid_node_owner) {
+ if (valid_node_owner == p_root) {
+ break;
+ }
+ valid_node_owner = valid_node_owner->get_owner();
+ }
+
+ if ((valid_node_owner == p_root && (p_root != p_edited_scene || !p_edited_scene->get_scene_file_path().is_empty())) || node_part_of_subscene || p_node == p_root) {
+ HashMap modified_properties = get_modified_properties_for_node(p_node);
+
+ // Find all valid connections to other nodes.
+ List connections_to;
+ p_node->get_all_signal_connections(&connections_to);
+
+ List valid_connections_to;
+ for (const Connection &c : connections_to) {
+ Node *connection_target_node = Object::cast_to(c.callable.get_object());
+ if (connection_target_node) {
+ // TODO: add support for reinstating custom callables
+ if (!c.callable.is_custom()) {
+ ConnectionWithNodePath connection_to;
+ connection_to.connection = c;
+ connection_to.node_path = p_node->get_path_to(connection_target_node);
+ valid_connections_to.push_back(connection_to);
+ }
+ }
+ }
+
+ // Find all valid connections from other nodes.
+ List connections_from;
+ p_node->get_signals_connected_to_this(&connections_from);
+
+ List valid_connections_from;
+ for (const Connection &c : connections_from) {
+ Node *source_node = Object::cast_to(c.signal.get_object());
+
+ Node *valid_source_owner = nullptr;
+ if (source_node) {
+ valid_source_owner = source_node->get_owner();
+ while (valid_source_owner) {
+ if (valid_source_owner == p_root) {
+ break;
+ }
+ valid_source_owner = valid_source_owner->get_owner();
+ }
+ }
+
+ if (!source_node || valid_source_owner == nullptr) {
+ // TODO: add support for reinstating custom callables
+ if (!c.callable.is_custom()) {
+ valid_connections_from.push_back(c);
+ }
+ }
+ }
+
+ // Find all node groups.
+ List groups;
+ p_node->get_groups(&groups);
+
+ if (!modified_properties.is_empty() || !valid_connections_to.is_empty() || !valid_connections_from.is_empty() || !groups.is_empty()) {
+ ModificationNodeEntry modification_node_entry;
+ modification_node_entry.property_table = modified_properties;
+ modification_node_entry.connections_to = valid_connections_to;
+ modification_node_entry.connections_from = valid_connections_from;
+ modification_node_entry.groups = groups;
+
+ p_modification_table[p_root->get_path_to(p_node)] = modification_node_entry;
+ }
+ } else {
+ AdditiveNodeEntry new_additive_node_entry;
+ new_additive_node_entry.node = p_node;
+ new_additive_node_entry.parent = p_root->get_path_to(p_node->get_parent());
+ new_additive_node_entry.owner = p_node->get_owner();
+ new_additive_node_entry.index = p_node->get_index();
+
+ //
+ {
+ // If it's a Node2D.
+ Node2D *node_2d = Object::cast_to(p_node);
+ if (node_2d) {
+ if (node_2d->is_inside_tree()) {
+ new_additive_node_entry.global_transform_2d = node_2d->get_global_transform();
+ } else {
+ new_additive_node_entry.global_transform_2d = get_canvas_item_relative_transform_without_tree(node_2d, p_edited_scene);
+ }
+ }
+ }
+ {
+ // If it's a Node3D.
+ Node3D *node_3d = Object::cast_to(p_node);
+ if (node_3d) {
+ if (node_3d->is_inside_tree()) {
+ new_additive_node_entry.global_transform_3d = node_3d->get_global_transform();
+ } else {
+ new_additive_node_entry.global_transform_3d = get_node_3d_relative_transform_without_tree(node_3d, p_edited_scene);
+ }
+ }
+ }
+ //
+
+ p_addition_list.push_back(new_additive_node_entry);
+
+ return;
+ }
+
+ for (int i = 0; i < p_node->get_child_count(); i++) {
+ Node *child = p_node->get_child(i);
+ update_diff_data_for_node(p_edited_scene, p_root, child, p_modification_table, p_addition_list);
+ }
+}
+//
+
void EditorNode::open_request(const String &p_path) {
if (!opening_prev) {
List::Element *prev_scene = previous_scenes.find(p_path);
@@ -5620,6 +5825,367 @@ void EditorNode::reload_scene(const String &p_path) {
scene_tabs->set_current_tab(current_tab);
}
+void EditorNode::find_all_instances_inheriting_path_in_node(Node *p_root, Node *p_node, const String &p_instance_path, List &p_instance_list) {
+ String scene_file_path = p_node->get_scene_file_path();
+
+ // This is going to get messy...
+ if (p_node->get_scene_file_path() == p_instance_path) {
+ p_instance_list.push_back(p_node);
+ } else {
+ Node *current_node = p_node;
+
+ Ref inherited_state = current_node->get_scene_inherited_state();
+ while (inherited_state.is_valid()) {
+ String inherited_path = inherited_state->get_path();
+ if (inherited_path == p_instance_path) {
+ p_instance_list.push_back(p_node);
+ break;
+ }
+
+ inherited_state = inherited_state->get_base_scene_state();
+ }
+ }
+
+ for (int i = 0; i < p_node->get_child_count(); i++) {
+ Node *child = p_node->get_child(i);
+ find_all_instances_inheriting_path_in_node(p_root, child, p_instance_path, p_instance_list);
+ }
+}
+
+void EditorNode::reload_instances_with_path_in_edited_scenes(const String &p_instance_path) {
+ int original_edited_scene_idx = editor_data.get_edited_scene();
+ HashMap> edited_scene_map;
+
+ // Walk through each opened scene to get a global list of all instances which match
+ // the current reimported scenes.
+ for (int i = 0; i < editor_data.get_edited_scene_count(); i++) {
+ if (editor_data.get_scene_path(i) != p_instance_path) {
+ Node *edited_scene_root = editor_data.get_edited_scene_root(i);
+
+ if (edited_scene_root) {
+ List valid_nodes;
+ find_all_instances_inheriting_path_in_node(edited_scene_root, edited_scene_root, p_instance_path, valid_nodes);
+ if (valid_nodes.size() > 0) {
+ edited_scene_map[i] = valid_nodes;
+ }
+ }
+ }
+ }
+
+ if (edited_scene_map.size() > 0) {
+ // Reload the new instance.
+ Error err;
+ Ref instance_scene_packed_scene = ResourceLoader::load(p_instance_path, "", ResourceFormatLoader::CACHE_MODE_IGNORE, &err);
+ instance_scene_packed_scene->set_path(p_instance_path, true);
+
+ ERR_FAIL_COND(err != OK);
+ ERR_FAIL_COND(instance_scene_packed_scene.is_null());
+
+ HashMap> local_scene_cache;
+ local_scene_cache[p_instance_path] = instance_scene_packed_scene;
+
+ for (const KeyValue> &edited_scene_map_elem : edited_scene_map) {
+ // Set the current scene.
+ int current_scene_idx = edited_scene_map_elem.key;
+ editor_data.set_edited_scene(current_scene_idx);
+ Node *current_edited_scene = editor_data.get_edited_scene_root(current_scene_idx);
+
+ // Clear the history for this tab (should we allow history to be retained?).
+ editor_data.get_undo_redo().clear_history();
+
+ // Update the version
+ editor_data.set_edited_scene_version(editor_data.get_scene_version(current_scene_idx) + 1, current_scene_idx);
+
+ for (Node *original_node : edited_scene_map_elem.value) {
+ // Walk the tree for the current node and extract relevant diff data, storing it in the modification table.
+ // For additional nodes which are part of the current scene, they get added to the addition table.
+ HashMap modification_table;
+ List addition_list;
+ update_diff_data_for_node(current_edited_scene, original_node, original_node, modification_table, addition_list);
+
+ // Disconnect all relevant connections, all connections from and persistent connections to.
+ for (const KeyValue &modification_table_entry : modification_table) {
+ for (Connection conn : modification_table_entry.value.connections_from) {
+ conn.signal.get_object()->disconnect(conn.signal.get_name(), conn.callable);
+ }
+ for (ConnectionWithNodePath cwnp : modification_table_entry.value.connections_to) {
+ Connection conn = cwnp.connection;
+ if (conn.flags & CONNECT_PERSIST) {
+ conn.signal.get_object()->disconnect(conn.signal.get_name(), conn.callable);
+ }
+ }
+ }
+
+ // Store all the paths for any selected nodes which are ancestors of the node we're replacing.
+ List selected_node_paths;
+ for (Node *selected_node : editor_selection->get_selected_node_list()) {
+ if (selected_node == original_node || original_node->is_ancestor_of(selected_node)) {
+ selected_node_paths.push_back(original_node->get_path_to(selected_node));
+ editor_selection->remove_node(selected_node);
+ }
+ }
+
+ // Remove all nodes which were added as additional elements (they will be restored later).
+ for (AdditiveNodeEntry additive_node_entry : addition_list) {
+ Node *addition_node = additive_node_entry.node;
+ addition_node->get_parent()->remove_child(addition_node);
+ }
+
+ // Clear ownership of the nodes (kind of hack to workaround an issue with
+ // replace_by when called on nodes in other tabs).
+ List nodes_owned_by_original_node;
+ original_node->get_owned_by(original_node, &nodes_owned_by_original_node);
+ for (Node *owned_node : nodes_owned_by_original_node) {
+ owned_node->set_owner(nullptr);
+ }
+
+ // Delete all the remaining node children.
+ while (original_node->get_child_count()) {
+ Node *child = original_node->get_child(0);
+
+ original_node->remove_child(child);
+ child->queue_delete();
+ }
+
+ // Reset the editable instance state.
+ bool is_editable = true;
+ Node *owner = original_node->get_owner();
+ if (owner) {
+ is_editable = owner->is_editable_instance(original_node);
+ }
+
+ // Load a replacement scene for the node.
+ Ref current_packed_scene;
+ if (original_node->get_scene_file_path() == p_instance_path) {
+ // If the node file name directly matches the scene we're replacing,
+ // just load it since we already cached it.
+ current_packed_scene = instance_scene_packed_scene;
+ } else {
+ // Otherwise, check the inheritance chain, reloading and caching any scenes
+ // we require along the way.
+ List required_load_paths;
+ String scene_path = original_node->get_scene_file_path();
+ // Do we need to check if the paths are empty?
+ if (!scene_path.is_empty()) {
+ required_load_paths.push_front(scene_path);
+ }
+ Ref inherited_state = original_node->get_scene_inherited_state();
+ while (inherited_state.is_valid()) {
+ String inherited_path = inherited_state->get_path();
+ // Do we need to check if the paths are empty?
+ if (!inherited_path.is_empty()) {
+ required_load_paths.push_front(inherited_path);
+ }
+ inherited_state = inherited_state->get_base_scene_state();
+ }
+
+ // Ensure the inheritance chain is loaded in the correct order so that cache can
+ // be properly updated.
+ for (String path : required_load_paths) {
+ if (!local_scene_cache.find(path)) {
+ current_packed_scene = ResourceLoader::load(path, "", ResourceFormatLoader::CACHE_MODE_IGNORE, &err);
+ current_packed_scene->set_path(path, true);
+ local_scene_cache[path] = current_packed_scene;
+ } else {
+ current_packed_scene = local_scene_cache[path];
+ }
+ }
+ }
+
+ ERR_FAIL_COND(current_packed_scene.is_null());
+
+ // Instantiate the node.
+ Node *instantiated_node = nullptr;
+ if (current_packed_scene.is_valid()) {
+ instantiated_node = current_packed_scene->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE);
+ }
+
+ ERR_FAIL_COND(!instantiated_node);
+
+ bool original_node_is_displayed_folded = original_node->is_displayed_folded();
+ bool original_node_scene_instance_load_placeholder = original_node->get_scene_instance_load_placeholder();
+
+ // Update the name to match
+ instantiated_node->set_name(original_node->get_name());
+
+ // Is this replacing the edited root node?
+ String original_node_file_path = original_node->get_scene_file_path();
+
+ if (current_edited_scene == original_node) {
+ instantiated_node->set_scene_file_path(original_node_file_path);
+ instantiated_node->set_scene_instance_state(original_node->get_scene_instance_state());
+ // Fix unsaved inherited scene
+ if (original_node_file_path.is_empty()) {
+ Ref state = current_packed_scene->get_state();
+ state->set_path(current_packed_scene->get_path());
+ instantiated_node->set_scene_inherited_state(state);
+ }
+ editor_data.set_edited_scene_root(instantiated_node);
+ current_edited_scene = instantiated_node;
+
+ if (original_node->is_inside_tree()) {
+ SceneTreeDock::get_singleton()->set_edited_scene(current_edited_scene);
+ original_node->get_tree()->set_edited_scene_root(instantiated_node);
+ }
+ }
+
+ // Replace the original node with the instantiated version.
+ original_node->replace_by(instantiated_node, false);
+
+ // Mark the old node for deletion.
+ original_node->queue_delete();
+
+ // Restore the folded and placeholder state from the original node.
+ instantiated_node->set_display_folded(original_node_is_displayed_folded);
+ instantiated_node->set_scene_instance_load_placeholder(original_node_scene_instance_load_placeholder);
+
+ if (owner) {
+ Ref ss_inst = owner->get_scene_instance_state();
+ if (ss_inst.is_valid()) {
+ ss_inst->update_instance_resource(p_instance_path, current_packed_scene);
+ }
+
+ owner->set_editable_instance(instantiated_node, is_editable);
+ }
+
+ // Attempt to re-add all the additional nodes.
+ for (AdditiveNodeEntry additive_node_entry : addition_list) {
+ Node *parent_node = instantiated_node->get_node_or_null(additive_node_entry.parent);
+
+ if (!parent_node) {
+ parent_node = current_edited_scene;
+ }
+
+ parent_node->add_child(additive_node_entry.node);
+ parent_node->move_child(additive_node_entry.node, additive_node_entry.index);
+ // If the additive node's owner was the node which got replaced, update it.
+ if (additive_node_entry.owner == original_node) {
+ additive_node_entry.owner = instantiated_node;
+ }
+
+ additive_node_entry.node->set_owner(additive_node_entry.owner);
+
+ // If the parent node was lost, attempt to restore the original global transform.
+ {
+ {
+ // If it's a Node2D.
+ Node2D *node_2d = Object::cast_to(additive_node_entry.node);
+ if (node_2d) {
+ if (node_2d->is_inside_tree()) {
+ node_2d->set_global_transform(additive_node_entry.global_transform_2d);
+ } else {
+ set_node_2d_relative_transform_without_tree(node_2d, current_edited_scene, additive_node_entry.global_transform_2d);
+ }
+ }
+ }
+
+ {
+ // If it's a Node3D.
+ Node3D *node_3d = Object::cast_to(additive_node_entry.node);
+ if (node_3d) {
+ if (node_3d->is_inside_tree()) {
+ node_3d->set_global_transform(additive_node_entry.global_transform_3d);
+ } else {
+ set_node_3d_relative_transform_without_tree(node_3d, current_edited_scene, additive_node_entry.global_transform_3d);
+ }
+ }
+ }
+ }
+ }
+
+ // Restore the selection.
+ if (selected_node_paths.size()) {
+ for (NodePath selected_node_path : selected_node_paths) {
+ Node *selected_node = instantiated_node->get_node_or_null(selected_node_path);
+ if (selected_node) {
+ editor_selection->add_node(selected_node);
+ }
+ }
+ editor_selection->update();
+ }
+
+ // Attempt to restore the modified properties and signals for the instantitated node and all its owned children.
+ for (KeyValue &E : modification_table) {
+ NodePath current_path = E.key;
+ Node *modifiable_node = instantiated_node->get_node_or_null(current_path);
+
+ if (modifiable_node) {
+ // Get properties for this node.
+ List pinfo;
+ modifiable_node->get_property_list(&pinfo);
+
+ // Get names of all valid property names (TODO: make this more efficent).
+ List property_names;
+ for (const PropertyInfo &E2 : pinfo) {
+ if (E2.usage & PROPERTY_USAGE_STORAGE) {
+ property_names.push_back(E2.name);
+ }
+ }
+
+ // Restore the modified properties for this node.
+ for (const KeyValue &E2 : E.value.property_table) {
+ if (property_names.find(E2.key)) {
+ modifiable_node->set(E2.key, E2.value);
+ }
+ }
+ // Restore the connections to other nodes.
+ for (const ConnectionWithNodePath &E2 : E.value.connections_to) {
+ Connection conn = E2.connection;
+
+ // Get the node the callable is targetting.
+ Node *target_node = cast_to(conn.callable.get_object());
+
+ // If the callable object no longer exists or is marked for deletion,
+ // attempt to reaccquire the closest match by using the node path
+ // we saved earlier.
+ if (!target_node || !target_node->is_queued_for_deletion()) {
+ target_node = modifiable_node->get_node_or_null(E2.node_path);
+ }
+
+ if (target_node) {
+ // Reconstruct the callable.
+ Callable new_callable = Callable(target_node, conn.callable.get_method());
+
+ if (!modifiable_node->is_connected(conn.signal.get_name(), new_callable)) {
+ ERR_FAIL_COND(modifiable_node->connect(conn.signal.get_name(), new_callable, conn.binds, conn.flags) != OK);
+ }
+ }
+ }
+
+ // Restore the connections from other nodes.
+ for (const Connection &E2 : E.value.connections_from) {
+ Connection conn = E2;
+
+ bool valid = modifiable_node->has_method(conn.callable.get_method()) || Ref