From 846bafd47fc3902f41aec532164ed8bd73ff9e83 Mon Sep 17 00:00:00 2001 From: Saracen Date: Tue, 21 May 2024 02:31:27 +0100 Subject: [PATCH 1/2] Changes the scene reimport function to handle some crash edge cases: * The reimported instance attempt to preserve ownerless nodes. * A recursive function call to '_nodes_scene_reimported' so these can be recreated if required. * Clears instance scene_state on new instantiated replacement nodes. --- editor/editor_node.cpp | 79 +++++++++++++++++++++++++----------------- editor/editor_node.h | 2 +- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index e4fcee22a750..2ddc97e069a4 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -4218,31 +4218,36 @@ void EditorNode::update_diff_data_for_node( 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(); + // Only save additional nodes which have an owner since this was causing issues transient ownerless nodes + // which get recreated upon scene tree entry. + // For now instead, assume all ownerless nodes are transient and will have to be recreated. + if (p_node->get_owner()) { + 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(); - Node2D *node_2d = Object::cast_to(p_node); - if (node_2d) { - new_additive_node_entry.transform_2d = node_2d->get_relative_transform_to_parent(node_2d->get_parent()); - } - Node3D *node_3d = Object::cast_to(p_node); - if (node_3d) { - new_additive_node_entry.transform_3d = node_3d->get_relative_transform(node_3d->get_parent()); - } + Node2D *node_2d = Object::cast_to(p_node); + if (node_2d) { + new_additive_node_entry.transform_2d = node_2d->get_relative_transform_to_parent(node_2d->get_parent()); + } + Node3D *node_3d = Object::cast_to(p_node); + if (node_3d) { + new_additive_node_entry.transform_3d = node_3d->get_relative_transform(node_3d->get_parent()); + } - // Gathers the ownership of all ancestor nodes for later use. - HashMap ownership_table; - for (int i = 0; i < p_node->get_child_count(); i++) { - Node *child = p_node->get_child(i); - update_ownership_table_for_addition_node_ancestors(child, ownership_table); - } + // Gathers the ownership of all ancestor nodes for later use. + HashMap ownership_table; + for (int i = 0; i < p_node->get_child_count(); i++) { + Node *child = p_node->get_child(i); + update_ownership_table_for_addition_node_ancestors(child, ownership_table); + } - new_additive_node_entry.ownership_table = ownership_table; + new_additive_node_entry.ownership_table = ownership_table; - p_addition_list.push_back(new_additive_node_entry); + p_addition_list.push_back(new_additive_node_entry); + } return; } @@ -5548,12 +5553,11 @@ void EditorNode::_file_access_close_error_notify_impl(const String &p_str) { add_io_error(vformat(TTR("Unable to write to file '%s', file in use, locked or lacking permissions."), p_str)); } -// Since we felt that a bespoke NOTIFICATION might not be desirable, this function -// provides the hardcoded callbacks to address known bugs which occur on certain -// nodes during reimport. -// Ideally, we should probably agree on a standardized method name which could be -// called from here instead. -void EditorNode::_notify_scene_updated(Node *p_node) { +// Recursive function to inform nodes that an array of nodes have had their scene reimported. +// It will attempt to call a method named '_nodes_scene_reimported' on every node in the +// tree so that editor scripts which create transient nodes will have the opportunity +// to recreate them. +void EditorNode::_notify_nodes_scene_reimported(Node *p_node, Array p_reimported_nodes) { Skeleton3D *skel_3d = Object::cast_to(p_node); if (skel_3d) { skel_3d->reset_bone_poses(); @@ -5564,8 +5568,12 @@ void EditorNode::_notify_scene_updated(Node *p_node) { } } + if (p_node->has_method("_nodes_scene_reimported")) { + p_node->call("_nodes_scene_reimported", p_reimported_nodes); + } + for (int i = 0; i < p_node->get_child_count(); i++) { - _notify_scene_updated(p_node->get_child(i)); + _notify_nodes_scene_reimported(p_node->get_child(i), p_reimported_nodes); } } @@ -5635,6 +5643,7 @@ void EditorNode::find_all_instances_inheriting_path_in_node(Node *p_root, Node * 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; + Array replaced_nodes; // Walk through each opened scene to get a global list of all instances which match // the current reimported scenes. @@ -5762,7 +5771,7 @@ void EditorNode::reload_instances_with_path_in_edited_scenes(const String &p_ins // be properly updated. for (String path : required_load_paths) { if (!local_scene_cache.find(path)) { - current_packed_scene = ResourceLoader::load(path, "", ResourceFormatLoader::CACHE_MODE_REPLACE, &err); + current_packed_scene = ResourceLoader::load(path, "", ResourceFormatLoader::CACHE_MODE_REPLACE_DEEP, &err); local_scene_cache[path] = current_packed_scene; } else { current_packed_scene = local_scene_cache[path]; @@ -5780,6 +5789,9 @@ void EditorNode::reload_instances_with_path_in_edited_scenes(const String &p_ins ERR_FAIL_NULL(instantiated_node); + // For clear instance state for path recaching. + instantiated_node->set_scene_instance_state(Ref()); + 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(); @@ -5949,13 +5961,18 @@ void EditorNode::reload_instances_with_path_in_edited_scenes(const String &p_ins } } } + // Add the newly instantiated node to the edited scene's replaced node list. + replaced_nodes.push_back(instantiated_node); } // Cleanup the history of the changes. editor_history.cleanup_history(); - - _notify_scene_updated(current_edited_scene); } + + // For the whole editor, call the _notify_nodes_scene_reimported with a list of replaced nodes. + // To inform anything that depends on them that they should update as appropriate. + _notify_nodes_scene_reimported(this, replaced_nodes); + edited_scene_map.clear(); } editor_data.set_edited_scene(original_edited_scene_idx); diff --git a/editor/editor_node.h b/editor/editor_node.h index 6b3359eaeeb7..800f562f112a 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -662,7 +662,7 @@ class EditorNode : public Node { void _begin_first_scan(); - void _notify_scene_updated(Node *p_node); + void _notify_nodes_scene_reimported(Node *p_node, Array p_reimported_nodes); protected: friend class FileSystemDock; From e57312d84eecb7672973a36c0938ed52a3b15474 Mon Sep 17 00:00:00 2001 From: Saracen Date: Thu, 23 May 2024 11:07:51 +0100 Subject: [PATCH 2/2] Updated scene hot-reloading: Preserves exported script variables Prevents corruption of direct node references. --- editor/editor_node.cpp | 271 ++++++++++++++++++++++++++--------------- editor/editor_node.h | 13 +- 2 files changed, 185 insertions(+), 99 deletions(-) diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 2ddc97e069a4..193d454a42d5 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -4107,7 +4107,7 @@ Error EditorNode::load_scene(const String &p_scene, bool p_ignore_broken_deps, b return OK; } -HashMap EditorNode::get_modified_properties_for_node(Node *p_node) { +HashMap EditorNode::get_modified_properties_for_node(Node *p_node, bool p_node_references_only) { HashMap modified_property_map; List pinfo; @@ -4119,7 +4119,17 @@ HashMap EditorNode::get_modified_properties_for_node(Node * 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; + // If this property is a direct node reference, save a NodePath instead to prevent corrupted references. + if (E.type == Variant::OBJECT && E.hint == PROPERTY_HINT_NODE_TYPE) { + Node *target_node = Object::cast_to(current_value); + if (target_node) { + modified_property_map[E.name] = p_node->get_path_to(target_node); + } + } else { + if (!p_node_references_only) { + modified_property_map[E.name] = current_value; + } + } } } } @@ -4137,10 +4147,118 @@ void EditorNode::update_ownership_table_for_addition_node_ancestors(Node *p_curr } } -void EditorNode::update_diff_data_for_node( - Node *p_edited_scene, +void EditorNode::update_node_from_node_modification_entry(Node *p_node, ModificationNodeEntry &p_node_modification) { + if (p_node) { + // First, attempt to restore the script property since it may affect the get_property_list method. + Variant *script_property_table_entry = p_node_modification.property_table.getptr(CoreStringName(script)); + if (script_property_table_entry) { + p_node->set_script(*script_property_table_entry); + } + + // Get properties for this node. + List pinfo; + p_node->get_property_list(&pinfo); + + // Get names of all valid property names. + HashMap property_node_reference_table; + for (const PropertyInfo &E : pinfo) { + if (E.usage & PROPERTY_USAGE_STORAGE) { + if (E.type == Variant::OBJECT && E.hint == PROPERTY_HINT_NODE_TYPE) { + property_node_reference_table[E.name] = true; + } else { + property_node_reference_table[E.name] = false; + } + } + } + + // Restore the modified properties for this node. + for (const KeyValue &E : p_node_modification.property_table) { + bool *property_node_reference_table_entry = property_node_reference_table.getptr(E.key); + if (property_node_reference_table_entry) { + // If the property is a node reference, attempt to restore from the node path instead. + bool is_node_reference = *property_node_reference_table_entry; + if (is_node_reference) { + if (E.value.get_type() == Variant::NODE_PATH) { + p_node->set(E.key, p_node->get_node_or_null(E.value)); + } + } else { + p_node->set(E.key, E.value); + } + } + } + + // Restore the connections to other nodes. + for (const ConnectionWithNodePath &E : p_node_modification.connections_to) { + Connection conn = E.connection; + + // Get the node the callable is targeting. + Node *target_node = Object::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 = p_node->get_node_or_null(E.node_path); + } + + if (target_node) { + // Reconstruct the callable. + Callable new_callable = Callable(target_node, conn.callable.get_method()); + + if (!p_node->is_connected(conn.signal.get_name(), new_callable)) { + ERR_FAIL_COND(p_node->connect(conn.signal.get_name(), new_callable, conn.flags) != OK); + } + } + } + + // Restore the connections from other nodes. + for (const Connection &E : p_node_modification.connections_from) { + Connection conn = E; + + bool valid = p_node->has_method(conn.callable.get_method()) || Ref