diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml index ae1eff4220b0..5b9f4a11bd71 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -982,6 +982,9 @@ An optional description to the node. It will be displayed as a tooltip when hovering over the node in the editor's Scene dock. + + Sets this node's state as an exposed node in its [member owner]. This allows the node to be accessed in the editor when the scene is instantiated in another. It also enforces that the node is unique, see [member unique_name_in_owner]. + The [MultiplayerAPI] instance associated with this node. See [method SceneTree.get_multiplayer]. [b]Note:[/b] Renaming the node, or moving it in the tree, will not move the [MultiplayerAPI] to the new path, you will have to update this manually. diff --git a/editor/editor_data.cpp b/editor/editor_data.cpp index 80c4c49c87f3..251e5f2e81a6 100644 --- a/editor/editor_data.cpp +++ b/editor/editor_data.cpp @@ -730,6 +730,9 @@ bool EditorData::check_and_update_scene(int p_idx) { } } + // Apply Overrides when instanced scene gets updated. + pscene->get_state()->apply_overrides(new_scene); + new_scene->set_scene_file_path(edited_scene[p_idx].root->get_scene_file_path()); Node *old_root = edited_scene[p_idx].root; EditorNode::get_singleton()->set_edited_scene(new_scene); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index c6144a34cb69..329e4a58d346 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -4193,6 +4193,8 @@ Error EditorNode::load_scene(const String &p_scene, bool p_ignore_broken_deps, b } } } + // Apply Overrides when loading a scene. + sdata->get_state()->apply_overrides(new_scene); if (!restoring_scenes) { save_editor_layout_delayed(); diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp index 52ba98b4d5c5..930f8bd338c0 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -177,6 +177,17 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i undo_redo->create_action(TTR("Disable Scene Unique Name")); undo_redo->add_do_method(n, "set_unique_name_in_owner", false); undo_redo->add_undo_method(n, "set_unique_name_in_owner", true); + if (n->is_exposed_in_owner()) { + undo_redo->add_do_method(n, "set_exposed_in_owner", false); + undo_redo->add_undo_method(n, "set_exposed_in_owner", true); + } + undo_redo->add_do_method(this, "_update_tree"); + undo_redo->add_undo_method(this, "_update_tree"); + undo_redo->commit_action(); + } else if (p_id == BUTTON_EXPOSED) { + undo_redo->create_action(TTR("Disable Scene Exposed Node")); + undo_redo->add_do_method(n, "set_exposed_in_owner", false); + undo_redo->add_undo_method(n, "set_exposed_in_owner", true); undo_redo->add_do_method(this, "_update_tree"); undo_redo->add_undo_method(this, "_update_tree"); undo_redo->commit_action(); @@ -203,9 +214,14 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { bool part_of_subscene = false; if (!display_foreign && p_node->get_owner() != get_scene_node() && p_node != get_scene_node()) { - if ((show_enabled_subscene || can_open_instance) && p_node->get_owner() && (get_scene_node()->is_editable_instance(p_node->get_owner()))) { + if (((show_enabled_subscene || can_open_instance) && p_node->get_owner() && (get_scene_node()->is_editable_instance(p_node->get_owner()))) || (p_node->get_owner() != get_scene_node() && p_node->is_exposed_in_owner())) { part_of_subscene = true; //allow + } else if (p_node->has_exposed_nodes(true)) { + for (int i = 0; i < p_node->get_child_count(); i++) { + _add_nodes(p_node->get_child(i), p_parent); + } + return; } else { return; } @@ -217,7 +233,7 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { item->set_text(0, p_node->get_name()); item->set_text_overrun_behavior(0, TextServer::OVERRUN_NO_TRIMMING); - if (can_rename && !part_of_subscene) { + if (can_rename && !part_of_subscene && (!p_node->is_exposed_in_owner() || p_node->get_owner() == get_scene_node())) { item->set_editable(0, true); } @@ -322,6 +338,15 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { item->add_button(0, get_editor_theme_icon(SNAME("SceneUniqueName")), BUTTON_UNIQUE, p_node->get_owner() != EditorNode::get_singleton()->get_edited_scene(), vformat(TTR("This node can be accessed from within anywhere in the scene by preceding it with the '%s' prefix in a node path.\nClick to disable this."), UNIQUE_NODE_PREFIX)); } + if (p_node->is_exposed_in_owner()) { + if (p_node->get_owner() == get_scene_node()) { + item->add_button(0, get_editor_theme_icon(SNAME("SceneExposedNode")), BUTTON_EXPOSED, p_node->get_owner() != EditorNode::get_singleton()->get_edited_scene(), TTR("This node will be exposed in the editor when this scene is instantiated.\nClick to disable this.")); + } else { + item->add_button(0, get_editor_theme_icon(SNAME("SceneExposedNode")), BUTTON_EXPOSED, p_node->get_owner() != EditorNode::get_singleton()->get_edited_scene(), TTR("This node has been exposed in the underlying scene.")); + _set_item_custom_color(item, get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); + } + } + int num_connections = p_node->get_persistent_signal_connection_count(); int num_groups = p_node->get_persistent_group_count(); diff --git a/editor/gui/scene_tree_editor.h b/editor/gui/scene_tree_editor.h index b4d9644f167a..5b27dc3a092e 100644 --- a/editor/gui/scene_tree_editor.h +++ b/editor/gui/scene_tree_editor.h @@ -55,6 +55,7 @@ class SceneTreeEditor : public Control { BUTTON_GROUPS = 7, BUTTON_PIN = 8, BUTTON_UNIQUE = 9, + BUTTON_EXPOSED = 10, }; Tree *tree = nullptr; diff --git a/editor/icons/SceneExposedNode.svg b/editor/icons/SceneExposedNode.svg new file mode 100644 index 000000000000..8603816c3ed6 --- /dev/null +++ b/editor/icons/SceneExposedNode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/import/3d/resource_importer_scene.cpp b/editor/import/3d/resource_importer_scene.cpp index 853e3e813b1b..1189ff5646a5 100644 --- a/editor/import/3d/resource_importer_scene.cpp +++ b/editor/import/3d/resource_importer_scene.cpp @@ -1024,6 +1024,11 @@ Node *ResourceImporterScene::_pre_fix_node(Node *p_node, Node *p_root, HashMapset_exposed_in_owner(true); + p_node->set_name(_fixstr(name, "exp")); + } + return p_node; } @@ -1976,9 +1981,11 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p switch (p_category) { case INTERNAL_IMPORT_CATEGORY_NODE: { r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); + r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/exposed_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); } break; case INTERNAL_IMPORT_CATEGORY_MESH_3D_NODE: { r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); + r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/exposed_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "generate/physics", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "generate/navmesh", PROPERTY_HINT_ENUM, "Disabled,Mesh + NavMesh,NavMesh Only"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "physics/body_type", PROPERTY_HINT_ENUM, "Static,Dynamic,Area"), 0)); @@ -2057,6 +2064,7 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p } break; case INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE: { r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); + r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/exposed_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "optimizer/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), true)); r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "optimizer/max_velocity_error", PROPERTY_HINT_RANGE, "0,1,0.01"), 0.01)); r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "optimizer/max_angular_error", PROPERTY_HINT_RANGE, "0,1,0.01"), 0.01)); @@ -2069,6 +2077,7 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p } break; case INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE: { r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); + r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/exposed_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "rest_pose/load_pose", PROPERTY_HINT_ENUM, "Default Pose,Use AnimationPlayer,Load External Animation", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::OBJECT, "rest_pose/external_animation_library", PROPERTY_HINT_RESOURCE_TYPE, "Animation,AnimationLibrary", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), Variant())); r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "rest_pose/selected_animation", PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), "")); @@ -2615,6 +2624,7 @@ Node *ResourceImporterScene::_generate_meshes(Node *p_node, const Dictionary &p_ } break; } + mesh_node->set_exposed_in_owner(src_mesh_node->is_exposed_in_owner()); mesh_node->set_layer_mask(src_mesh_node->get_layer_mask()); mesh_node->set_cast_shadows_setting(src_mesh_node->get_cast_shadows_setting()); mesh_node->set_visibility_range_begin(src_mesh_node->get_visibility_range_begin()); diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 02c8a03ac6ed..29265442a221 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -208,6 +208,8 @@ void SceneTreeDock::shortcut_input(const Ref &p_event) { _tool_selected(TOOL_SHOW_IN_FILE_SYSTEM); } else if (ED_IS_SHORTCUT("scene_tree/toggle_unique_name", p_event)) { _tool_selected(TOOL_TOGGLE_SCENE_UNIQUE_NAME); + } else if (ED_IS_SHORTCUT("scene_tree/toggle_exposed_node", p_event)) { + _tool_selected(TOOL_TOGGLE_SCENE_EXPOSED); } else if (ED_IS_SHORTCUT("scene_tree/toggle_editable_children", p_event)) { _tool_selected(TOOL_SCENE_EDITABLE_CHILDREN); } else if (ED_IS_SHORTCUT("scene_tree/delete", p_event)) { @@ -1424,6 +1426,82 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { } undo_redo->add_do_method(node, "set_unique_name_in_owner", false); undo_redo->add_undo_method(node, "set_unique_name_in_owner", true); + if (node->is_exposed_in_owner()) { + undo_redo->add_undo_method(node, "set_exposed_in_owner", true); + } + } + undo_redo->commit_action(); + } + } break; + case TOOL_TOGGLE_SCENE_EXPOSED: { + // Enabling/disabling based on the same node based on which the checkbox in the menu is checked/unchecked. + List::Element *first_selected = editor_selection->get_selected_node_list().front(); + if (first_selected == nullptr) { + return; + } + if (first_selected->get() == EditorNode::get_singleton()->get_edited_scene()) { + // Exclude Root Node. It should never be unique name in its own scene! + editor_selection->remove_node(first_selected->get()); + first_selected = editor_selection->get_selected_node_list().front(); + if (first_selected == nullptr) { + return; + } + } + bool enabling = !first_selected->get()->is_exposed_in_owner(); + + List full_selection = editor_selection->get_full_selected_node_list(); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + if (enabling) { + Vector new_exposed_nodes; + Vector new_unique_names; + Vector cant_be_set_unique_names; + + for (Node *node : full_selection) { + if (node->is_exposed_in_owner()) { + continue; + } + if (node->get_owner() != get_tree()->get_edited_scene_root()) { + continue; + } + + StringName name = node->get_name(); + Node *unique_node = get_tree()->get_edited_scene_root()->get_node_or_null(UNIQUE_NODE_PREFIX + String(name)); + if (new_unique_names.has(name) || (unique_node != nullptr && unique_node != this && unique_node->is_exposed_in_owner())) { + cant_be_set_unique_names.push_back(name); + } else { + new_exposed_nodes.push_back(node); + new_unique_names.push_back(name); + } + } + if (new_exposed_nodes.size()) { + undo_redo->create_action(TTR("Enable Exposed Node(s)")); + for (Node *node : new_exposed_nodes) { + if (!node->is_unique_name_in_owner()) { + undo_redo->add_undo_method(node, "set_unique_name_in_owner", false); + } + undo_redo->add_do_method(node, "set_exposed_in_owner", true); + undo_redo->add_undo_method(node, "set_exposed_in_owner", false); + } + undo_redo->commit_action(); + } + if (cant_be_set_unique_names.size()) { + String popup_text = TTR("Unique names already used by another node in the scene:"); + popup_text += "\n"; + for (const StringName &name : cant_be_set_unique_names) { + popup_text += "\n" + String(name); + } + accept->set_text(popup_text); + accept->popup_centered(); + } + } else { // Disabling. + undo_redo->create_action(TTR("Disable Exposed Node(s)")); + for (Node *node : full_selection) { + if (!node->is_unique_name_in_owner()) { + continue; + } + undo_redo->add_do_method(node, "set_exposed_in_owner", false); + undo_redo->add_undo_method(node, "set_exposed_in_owner", true); } undo_redo->commit_action(); } @@ -2601,7 +2679,7 @@ void SceneTreeDock::_toggle_editable_children(Node *p_node) { Array name_array; for (Node *owned_node : owned) { - if (owned_node != p_node && owned_node != edited_scene && owned_node->get_owner() == edited_scene && owned_node->get_parent()->get_owner() != edited_scene) { + if (owned_node != p_node && owned_node != edited_scene && owned_node->get_owner() == edited_scene && owned_node->get_parent()->get_owner() != edited_scene && owned_node->is_exposed_in_owner()) { owned_nodes_array.push_back(owned_node); paths_array.push_back(p_node->get_path_to(owned_node->get_parent())); name_array.push_back(owned_node->get_name()); @@ -3673,6 +3751,8 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) { Node *node = full_selection.front()->get(); menu->add_icon_shortcut(get_editor_theme_icon(SNAME("SceneUniqueName")), ED_GET_SHORTCUT("scene_tree/toggle_unique_name"), TOOL_TOGGLE_SCENE_UNIQUE_NAME); menu->set_item_text(menu->get_item_index(TOOL_TOGGLE_SCENE_UNIQUE_NAME), node->is_unique_name_in_owner() ? TTR("Revoke Unique Name") : TTR("Access as Unique Name")); + menu->add_icon_shortcut(get_editor_theme_icon(SNAME("SceneExposedNode")), ED_GET_SHORTCUT("scene_tree/toggle_exposed_node"), TOOL_TOGGLE_SCENE_EXPOSED); + menu->set_item_text(menu->get_item_index(TOOL_TOGGLE_SCENE_EXPOSED), node->is_exposed_in_owner() ? TTR("Unexpose Node in Instantiated Scenes") : TTR("Expose Node in Instantiated Scenes")); } } @@ -4430,6 +4510,7 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec ED_SHORTCUT("scene_tree/show_in_file_system", TTR("Show in FileSystem")); ED_SHORTCUT("scene_tree/toggle_unique_name", TTR("Toggle Access as Unique Name")); ED_SHORTCUT("scene_tree/toggle_editable_children", TTR("Toggle Editable Children")); + ED_SHORTCUT("scene_tree/toggle_exposed_node", TTR("Toggle node as Exposed in scene")); ED_SHORTCUT("scene_tree/delete_no_confirm", TTR("Delete (No Confirm)"), KeyModifierMask::SHIFT | Key::KEY_DELETE); ED_SHORTCUT("scene_tree/delete", TTR("Delete"), Key::KEY_DELETE); diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h index f01e6598cf37..3848beb2f42f 100644 --- a/editor/scene_tree_dock.h +++ b/editor/scene_tree_dock.h @@ -90,6 +90,7 @@ class SceneTreeDock : public VBoxContainer { TOOL_SCENE_CLEAR_INHERITANCE_CONFIRM, TOOL_SCENE_OPEN_INHERITED, TOOL_TOGGLE_SCENE_UNIQUE_NAME, + TOOL_TOGGLE_SCENE_EXPOSED, TOOL_CREATE_2D_SCENE, TOOL_CREATE_3D_SCENE, TOOL_CREATE_USER_INTERFACE, diff --git a/scene/main/node.cpp b/scene/main/node.cpp index de6d49761b53..f114708a66f5 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -1894,6 +1894,59 @@ TypedArray Node::find_children(const String &p_pattern, const String &p_ty return ret; } +bool Node::is_child_of_exposed_node(const Node *p_owner) const { + HashSet visited; + if (p_owner) { + const Node *n = p_owner->get_parent(); + while (n) { + visited.insert(n); + n = n->data.parent; + } + } + const Node *common_parent = data.parent; + while (common_parent) { + if (!common_parent->get_scene_file_path().is_empty() || (p_owner && visited.has(common_parent))) { + break; + } + if (common_parent->is_exposed_in_owner()) { + return true; + } + common_parent = common_parent->data.parent; + } + return false; +} + +bool Node::has_exposed_nodes(bool p_recursive) const { + _update_children_cache(); + Node *const *cptr = data.children_cache.ptr(); + int ccount = data.children_cache.size(); + for (int i = 0; i < ccount; i++) { + if (!cptr[i]->data.owner) { + continue; + } + if (cptr[i]->is_exposed_in_owner()) { + return true; + } + if (p_recursive && cptr[i]->has_exposed_nodes(true)) { + return true; + } + } + return false; +} + +TypedArray Node::get_owned_exposed_nodes(bool p_recursive) const { + ERR_THREAD_GUARD_V(TypedArray()) + TypedArray ret; + for (Node *node : data.owned_exposed_nodes) { + ret.append(node); + if (p_recursive) { + ret.append_array(node->get_owned_exposed_nodes(true)); + } + } + + return ret; +} + void Node::reparent(Node *p_parent, bool p_keep_global_transform) { ERR_THREAD_GUARD ERR_FAIL_NULL(p_parent); @@ -2066,6 +2119,20 @@ void Node::_set_owner_nocheck(Node *p_owner) { owner_changed_notify(); } +void Node::_release_exposed_in_owner() { + data.owner->data.owned_exposed_nodes.erase(this); +} + +void Node::_acquire_exposed_in_owner() { + ERR_FAIL_NULL(data.owner); // Safety check. + if (data.owner->data.owned_exposed_nodes.has(this)) { + WARN_PRINT(vformat("Setting node name '%s' to be exposed within scene for '%s', but it's already claimed by '%s'.\n'%s' is no longer set as being exposed.", + get_name(), is_inside_tree() ? get_path() : data.owner->get_path_to(this))); + return; + } + data.owner->data.owned_exposed_nodes.insert(this); +} + void Node::_release_unique_name_in_owner() { ERR_FAIL_NULL(data.owner); // Safety check. StringName key = StringName(UNIQUE_NODE_PREFIX + data.name.operator String()); @@ -2085,6 +2152,11 @@ void Node::_acquire_unique_name_in_owner() { WARN_PRINT(vformat("Setting node name '%s' to be unique within scene for '%s', but it's already claimed by '%s'.\n'%s' is no longer set as having a unique name.", get_name(), is_inside_tree() ? get_path() : data.owner->get_path_to(this), which_path, which_path)); data.unique_name_in_owner = false; + if (data.exposed_in_owner) { + WARN_PRINT(vformat("Setting node name '%s' to be exposed within scene for '%s', but it's already claimed by '%s'.\n'%s' is no longer set as being exposed.", + get_name(), is_inside_tree() ? get_path() : data.owner->get_path_to(this), which_path, which_path)); + data.exposed_in_owner = false; + } return; } data.owner->data.owned_unique_nodes[key] = this; @@ -2096,6 +2168,10 @@ void Node::set_unique_name_in_owner(bool p_enabled) { return; } + if (!p_enabled && data.exposed_in_owner) { + set_exposed_in_owner(false); + } + if (data.unique_name_in_owner && data.owner != nullptr) { _release_unique_name_in_owner(); } @@ -2112,6 +2188,28 @@ bool Node::is_unique_name_in_owner() const { return data.unique_name_in_owner; } +void Node::set_exposed_in_owner(bool p_enabled) { + ERR_MAIN_THREAD_GUARD + if (data.exposed_in_owner == p_enabled) { + return; + } + if (data.exposed_in_owner && data.owner != nullptr) { + _release_exposed_in_owner(); + } + data.exposed_in_owner = p_enabled; + if (p_enabled) { + set_unique_name_in_owner(p_enabled); + } + if (data.exposed_in_owner && data.owner != nullptr) { + _acquire_exposed_in_owner(); + } + update_configuration_warnings(); +} + +bool Node::is_exposed_in_owner() const { + return data.exposed_in_owner; +} + void Node::set_owner(Node *p_owner) { ERR_MAIN_THREAD_GUARD if (data.owner) { @@ -2154,6 +2252,9 @@ void Node::_clean_up_owner() { if (data.unique_name_in_owner) { _release_unique_name_in_owner(); + if (data.exposed_in_owner) { + _release_exposed_in_owner(); + } } data.owner->data.owned.erase(data.OW); data.owner = nullptr; @@ -2222,17 +2323,30 @@ NodePath Node::get_path_to(const Node *p_node, bool p_use_unique_path) const { Vector path; StringName up = String(".."); - if (p_use_unique_path) { + if (p_use_unique_path || p_node->is_exposed_in_owner() || p_node->is_child_of_exposed_node(this)) { n = p_node; bool is_detected = false; + const Node *exposed = nullptr; while (n != common_parent) { - if (n->is_unique_name_in_owner() && n->get_owner() == get_owner()) { + if (n->is_exposed_in_owner()) { + exposed = n; path.push_back(UNIQUE_NODE_PREFIX + String(n->get_name())); - is_detected = true; - break; } - path.push_back(n->get_name()); + if (!exposed) { + if (n->is_unique_name_in_owner() && n->get_owner() == get_owner()) { + path.push_back(UNIQUE_NODE_PREFIX + String(n->get_name())); + is_detected = true; + break; + } + path.push_back(n->get_name()); + } else { + bool accessible = (n == exposed) || (n->get_owner() && n->get_owner()->get_node_or_null(NodePath(UNIQUE_NODE_PREFIX + String(exposed->get_name())))); + if (!accessible) { + exposed = nullptr; + path.push_back(n->get_name()); + } + } n = n->data.parent; } @@ -2527,6 +2641,10 @@ void Node::set_editable_instance(Node *p_node, bool p_editable) { ERR_THREAD_GUARD ERR_FAIL_NULL(p_node); ERR_FAIL_COND(!is_ancestor_of(p_node)); + if (is_exposed_in_owner()) { + p_node->data.editable_instance = true; + return; + } if (!p_editable) { p_node->data.editable_instance = false; // Avoid this flag being needlessly saved; @@ -3650,6 +3768,9 @@ void Node::_bind_methods() { ClassDB::bind_method(D_METHOD("set_unique_name_in_owner", "enable"), &Node::set_unique_name_in_owner); ClassDB::bind_method(D_METHOD("is_unique_name_in_owner"), &Node::is_unique_name_in_owner); + ClassDB::bind_method(D_METHOD("set_exposed_in_owner", "enable"), &Node::set_exposed_in_owner); + ClassDB::bind_method(D_METHOD("is_exposed_in_owner"), &Node::is_exposed_in_owner); + ClassDB::bind_method(D_METHOD("atr", "message", "context"), &Node::atr, DEFVAL("")); ClassDB::bind_method(D_METHOD("atr_n", "message", "plural_message", "n", "context"), &Node::atr_n, DEFVAL("")); @@ -3786,6 +3907,7 @@ void Node::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_name", "get_name"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "unique_name_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_unique_name_in_owner", "is_unique_name_in_owner"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "exposed_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_exposed_in_owner", "is_exposed_in_owner"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "scene_file_path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_scene_file_path", "get_scene_file_path"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "owner", PROPERTY_HINT_RESOURCE_TYPE, "Node", PROPERTY_USAGE_NONE), "set_owner", "get_owner"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "multiplayer", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerAPI", PROPERTY_USAGE_NONE), "", "get_multiplayer"); diff --git a/scene/main/node.h b/scene/main/node.h index e4124591059e..6677a83b3e76 100644 --- a/scene/main/node.h +++ b/scene/main/node.h @@ -169,7 +169,9 @@ class Node : public Object { mutable bool children_cache_dirty = true; mutable LocalVector children_cache; HashMap owned_unique_nodes; + HashSet owned_exposed_nodes; bool unique_name_in_owner = false; + bool exposed_in_owner = false; InternalMode internal_mode = INTERNAL_MODE_DISABLED; mutable int internal_children_front_count_cache = 0; mutable int internal_children_back_count_cache = 0; @@ -300,6 +302,8 @@ class Node : public Object { _FORCE_INLINE_ bool _can_process(bool p_paused) const; _FORCE_INLINE_ bool _is_enabled() const; + void _release_exposed_in_owner(); + void _acquire_exposed_in_owner(); void _release_unique_name_in_owner(); void _acquire_unique_name_in_owner(); @@ -457,6 +461,9 @@ class Node : public Object { Node *get_node_or_null(const NodePath &p_path) const; Node *find_child(const String &p_pattern, bool p_recursive = true, bool p_owned = true) const; TypedArray find_children(const String &p_pattern, const String &p_type = "", bool p_recursive = true, bool p_owned = true) const; + bool is_child_of_exposed_node(const Node *p_owner = nullptr) const; + bool has_exposed_nodes(bool p_recursive) const; + TypedArray get_owned_exposed_nodes(bool p_recursive = true) const; bool has_node_and_resource(const NodePath &p_path) const; Node *get_node_and_resource(const NodePath &p_path, Ref &r_res, Vector &r_leftover_subpath, bool p_last_is_property = true) const; @@ -503,6 +510,9 @@ class Node : public Object { void set_unique_name_in_owner(bool p_enabled); bool is_unique_name_in_owner() const; + void set_exposed_in_owner(bool p_enabled); + bool is_exposed_in_owner() const; + _FORCE_INLINE_ int get_index(bool p_include_internal = true) const { // p_include_internal = false doesn't make sense if the node is internal. ERR_FAIL_COND_V_MSG(!p_include_internal && data.internal_mode != INTERNAL_MODE_DISABLED, -1, "Node is internal. Can't get index with 'include_internal' being false."); diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp index 27db65bb1ab3..5e390f0a07a2 100644 --- a/scene/resources/packed_scene.cpp +++ b/scene/resources/packed_scene.cpp @@ -180,6 +180,12 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const { old_parent_path = String(node_paths[n.parent & FLAG_MASK]).trim_prefix("./").replace("/", "@"); nparent = ret_nodes[0]; } + NODE_FROM_ID(nowner, n.owner); + if (nowner && nparent && nparent->get_owner() && nowner != nparent->get_owner() && !nparent->is_exposed_in_owner()) { + WARN_PRINT(String("Exposed parent path '" + String(node_paths[n.parent & FLAG_MASK]) + "' for node '" + String(snames[n.name]) + "' has vanished when instantiating: '" + get_path() + "'.").ascii().get_data()); + old_parent_path = String(node_paths[n.parent & FLAG_MASK]).trim_prefix("./").replace("/", "@"); + nparent = ret_nodes[0]; + } #endif parent = nparent; } else { @@ -501,6 +507,9 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const { if (node->data.unique_name_in_owner) { node->_acquire_unique_name_in_owner(); } + if (node->data.exposed_in_owner) { + node->_acquire_exposed_in_owner(); + } } } @@ -604,6 +613,16 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const { } } + // Apply Overrides. + for (int i = 0; i < overrides.size(); i++) { + Node *ei = ret_nodes[0]->get_node_or_null(get_override_path(i)); + if (ei != nullptr) { + const Vector &override_properties = overrides[i].properties; + for (const NodeData::Property &prop : override_properties) { + ei->set(names[prop.name], variants[prop.value]); + } + } + } return ret_nodes[0]; } @@ -678,21 +697,17 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has // document it. if you fail to understand something, please ask! //discard nodes that do not belong to be processed - if (p_node != p_owner && p_node->get_owner() != p_owner && !p_owner->is_editable_instance(p_node->get_owner())) { + if (p_node != p_owner && p_node->get_owner() != p_owner && !p_owner->is_editable_instance(p_node->get_owner()) && !p_node->is_exposed_in_owner()) { return OK; } - bool is_editable_instance = false; + bool root_editable_instance = p_node != p_owner && !p_node->get_scene_file_path().is_empty() && p_owner->is_editable_instance(p_node); + bool is_editable_instance = root_editable_instance || (p_node->get_owner() && p_owner->is_ancestor_of(p_node->get_owner()) && p_owner->is_editable_instance(p_node->get_owner())); // save the child instantiated scenes that are chosen as editable, so they can be restored // upon load back - if (p_node != p_owner && !p_node->get_scene_file_path().is_empty() && p_owner->is_editable_instance(p_node)) { + if (root_editable_instance) { editable_instances.push_back(p_owner->get_path_to(p_node)); - // Node is the root of an editable instance. - is_editable_instance = true; - } else if (p_node->get_owner() && p_owner->is_ancestor_of(p_node->get_owner()) && p_owner->is_editable_instance(p_node->get_owner())) { - // Node is part of an editable instance. - is_editable_instance = true; } NodeData nd; @@ -898,6 +913,8 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has nd.type = TYPE_INSTANTIATED; } + bool is_override = p_node->is_exposed_in_owner() && p_owner != p_node->get_owner() && !is_editable_instance && nd.properties.size() > 0; + // determine whether to save this node or not // if this node is part of an instantiated sub-scene, we can skip storing it if basically // no properties changed and no groups were added to it. @@ -907,6 +924,7 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has bool save_node = nd.properties.size() || nd.groups.size(); // some local properties or groups exist save_node = save_node || p_node == p_owner; // owner is always saved save_node = save_node || (p_node->get_owner() == p_owner && instantiated_by_owner); //part of scene and not instanced + save_node = save_node && !is_override; int idx = nodes.size(); int parent_node = NO_PARENT_SAVED; @@ -935,11 +953,34 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has nodes.push_back(nd); } + // Save the exposed node's overridden properties (if any). + if (is_override) { + int pidx = add_override_path(p_owner->get_path_to(p_node, true)); + SceneState::OverrideData od; + od.path = pidx; + od.properties = nd.properties; + od.groups = nd.groups; + overrides.push_back(od); + } + + // Parse all child nodes that belong to p_owner or are not exposed. for (int i = 0; i < p_node->get_child_count(); i++) { Node *c = p_node->get_child(i); - Error err = _parse_node(p_owner, c, parent_node, name_map, variant_map, node_map, nodepath_map); - if (err) { - return err; + if (!c->is_exposed_in_owner() || is_editable_instance || c->get_owner() == p_owner) { + Error err = _parse_node(p_owner, c, parent_node, name_map, variant_map, node_map, nodepath_map); + if (err) { + return err; + } + } + } + + // Parse any owned exposed nodes if this node is an instantiated scene and "Editable Children" is not enabled. + if (nd.type == TYPE_INSTANTIATED && !root_editable_instance) { + for (Variant node : p_node->get_owned_exposed_nodes(false)) { + Error err = _parse_node(p_owner, Object::cast_to(node), parent_node, name_map, variant_map, node_map, nodepath_map); + if (err) { + return err; + } } } @@ -1193,7 +1234,7 @@ Error SceneState::pack(Node *p_scene) { node_paths.resize(nodepath_map.size()); for (const KeyValue &E : nodepath_map) { - node_paths.write[E.value] = scene->get_path_to(E.key); + node_paths.write[E.value] = scene->get_path_to(E.key, true); } if (Engine::get_singleton()->is_editor_hint()) { @@ -1534,6 +1575,38 @@ void SceneState::set_bundled_scene(const Dictionary &p_dictionary) { editable_instances.write[i] = ei[i]; } + Array op; + if (p_dictionary.has("override_paths")) { + op = p_dictionary["override_paths"]; + } + override_paths.resize(op.size()); + for (int i = 0; i < op.size(); i++) { + override_paths.write[i] = op[i]; + } + + const int override_count = override_paths.size(); + const Vector onodes = p_dictionary["overrides"]; + ERR_FAIL_COND(onodes.size() < override_count); + + overrides.resize(override_count); + if (override_count) { + const int *r = onodes.ptr(); + int idx = 0; + for (int i = 0; i < override_count; i++) { + OverrideData &od = overrides.write[i]; + od.path = r[idx++]; + od.properties.resize(r[idx++]); + for (int j = 0; j < od.properties.size(); j++) { + od.properties.write[j].name = r[idx++]; + od.properties.write[j].value = r[idx++]; + } + od.groups.resize(r[idx++]); + for (int j = 0; j < od.groups.size(); j++) { + od.groups.write[j] = r[idx++]; + } + } + } + //path=p_dictionary["path"]; } @@ -1616,6 +1689,29 @@ Dictionary SceneState::get_bundled_scene() const { d["base_scene"] = base_scene_idx; } + Array onode_paths; + onode_paths.resize(override_paths.size()); + for (int i = 0; i < override_paths.size(); i++) { + onode_paths[i] = override_paths[i]; + } + d["override_paths"] = onode_paths; + Vector onodes; + for (int i = 0; i < overrides.size(); i++) { + const OverrideData &od = overrides[i]; + onodes.push_back(od.path); + onodes.push_back(od.properties.size()); + for (int j = 0; j < od.properties.size(); j++) { + onodes.push_back(od.properties[j].name); + onodes.push_back(od.properties[j].value); + } + onodes.push_back(od.groups.size()); + for (int j = 0; j < od.groups.size(); j++) { + onodes.push_back(od.groups[j]); + } + } + + d["overrides"] = onodes; + d["version"] = PACKED_SCENE_VERSION; return d; @@ -1890,6 +1986,61 @@ Ref SceneState::get_sub_resource(const String &p_path) { return Ref(); } +Vector SceneState::get_overrides() const { + return overrides; +} + +int SceneState::get_override_count() const { + return overrides.size(); +} + +Vector SceneState::get_override_groups(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, overrides.size(), Vector()); + Vector groups; + for (int i = 0; i < overrides[p_idx].groups.size(); i++) { + groups.push_back(names[overrides[p_idx].groups[i]]); + } + return groups; +} + +NodePath SceneState::get_override_path(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, overrides.size(), NodePath()); + return override_paths[overrides[p_idx].path]; +} + +int SceneState::get_override_node_property_count(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, overrides.size(), -1); + return overrides[p_idx].properties.size(); +} + +Variant SceneState::get_override_property_value(int p_idx, int p_prop) const { + ERR_FAIL_INDEX_V(p_idx, overrides.size(), Variant()); + ERR_FAIL_INDEX_V(p_prop, overrides[p_idx].properties.size(), Variant()); + + return variants[overrides[p_idx].properties[p_prop].value]; +} + +StringName SceneState::get_override_property_name(int p_idx, int p_prop) const { + ERR_FAIL_INDEX_V(p_idx, overrides.size(), StringName()); + ERR_FAIL_INDEX_V(p_prop, overrides[p_idx].properties.size(), StringName()); + return names[overrides[p_idx].properties[p_prop].name & FLAG_PROP_NAME_MASK]; +} + +void SceneState::apply_overrides(Node *p_scene) const { + for (int i = 0; i < overrides.size(); i++) { + SceneState::OverrideData o_data = overrides[i]; + NodePath np = get_override_path(i); + Node *ei = p_scene->get_node_or_null(np); + if (ei == nullptr) { + WARN_PRINT("Exposed node not found in the scene: " + np); + continue; + } + for (int j = 0; j < o_data.properties.size(); ++j) { + ei->set(names[o_data.properties[j].name], variants[o_data.properties[j].value]); + } + } +} + //add int SceneState::add_name(const StringName &p_name) { @@ -1907,6 +2058,11 @@ int SceneState::add_node_path(const NodePath &p_path) { return (node_paths.size() - 1) | FLAG_ID_IS_PATH; } +int SceneState::add_override_path(const NodePath &p_path) { + override_paths.push_back(p_path); + return (override_paths.size() - 1); +} + int SceneState::add_node(int p_parent, int p_owner, int p_type, int p_name, int p_instance, int p_index) { NodeData nd; nd.parent = p_parent; @@ -1968,6 +2124,33 @@ void SceneState::add_editable_instance(const NodePath &p_path) { editable_instances.push_back(p_path); } +int SceneState::add_override(int p_path) { + OverrideData nd; + nd.path = p_path; + overrides.push_back(nd); + return overrides.size() - 1; +} + +void SceneState::add_override_property(int p_node, int p_name, int p_value, bool p_deferred_node_path) { + ERR_FAIL_INDEX(p_node, overrides.size()); + ERR_FAIL_INDEX(p_name, names.size()); + ERR_FAIL_INDEX(p_value, variants.size()); + + NodeData::Property prop; + prop.name = p_name; + if (p_deferred_node_path) { + prop.name |= FLAG_PATH_PROPERTY_IS_NODE; + } + prop.value = p_value; + overrides.write[p_node].properties.push_back(prop); +} + +void SceneState::add_override_group(int p_node, int p_group) { + ERR_FAIL_INDEX(p_node, overrides.size()); + ERR_FAIL_INDEX(p_group, names.size()); + overrides.write[p_node].groups.push_back(p_group); +} + bool SceneState::remove_group_references(const StringName &p_name) { bool edited = false; for (NodeData &node : nodes) { diff --git a/scene/resources/packed_scene.h b/scene/resources/packed_scene.h index d27def1760e8..5a57b3b52a2d 100644 --- a/scene/resources/packed_scene.h +++ b/scene/resources/packed_scene.h @@ -40,6 +40,7 @@ class SceneState : public RefCounted { Vector names; Vector variants; Vector node_paths; + Vector override_paths; Vector editable_instances; mutable HashMap node_path_cache; mutable HashMap base_scene_node_remap; @@ -69,6 +70,12 @@ class SceneState : public RefCounted { Vector groups; }; + struct OverrideData { + int path = 0; + Vector properties; + Vector groups; + }; + struct DeferredNodePathProperties { Node *base = nullptr; StringName property; @@ -76,6 +83,7 @@ class SceneState : public RefCounted { }; Vector nodes; + Vector overrides; struct ConnectionData { int from = 0; @@ -197,17 +205,30 @@ class SceneState : public RefCounted { Vector get_editable_instances() const; Ref get_sub_resource(const String &p_path); + Vector get_overrides() const; + int get_override_count() const; + Vector get_override_groups(int p_idx) const; + NodePath get_override_path(int p_idx) const; + void apply_overrides(Node *p_scene) const; + StringName get_override_property_name(int p_idx, int p_prop) const; + int get_override_node_property_count(int p_idx) const; + Variant get_override_property_value(int p_idx, int p_prop) const; + //build API int add_name(const StringName &p_name); int add_value(const Variant &p_value); int add_node_path(const NodePath &p_path); + int add_override_path(const NodePath &p_path); int add_node(int p_parent, int p_owner, int p_type, int p_name, int p_instance, int p_index); void add_node_property(int p_node, int p_name, int p_value, bool p_deferred_node_path = false); void add_node_group(int p_node, int p_group); void set_base_scene(int p_idx); void add_connection(int p_from, int p_to, int p_signal, int p_method, int p_flags, int p_unbinds, const Vector &p_binds); void add_editable_instance(const NodePath &p_path); + int add_override(int p_path); + void add_override_property(int p_node, int p_name, int p_value, bool p_deferred_node_path); + void add_override_group(int p_node, int p_group); bool remove_group_references(const StringName &p_name); bool rename_group_references(const StringName &p_old_name, const StringName &p_new_name); diff --git a/scene/resources/resource_format_text.cpp b/scene/resources/resource_format_text.cpp index 90102e44e4a9..8a4e99517749 100644 --- a/scene/resources/resource_format_text.cpp +++ b/scene/resources/resource_format_text.cpp @@ -397,6 +397,45 @@ Ref ResourceLoaderText::_parse_node_tag(VariantParser::ResourcePars return packed_scene; } } + } else if (next_tag.name == "override") { + NodePath path = next_tag.fields["path"]; + HashSet path_properties; + int o_path = packed_scene->get_state()->add_override_path(path); + int override_id = packed_scene->get_state()->add_override(o_path); + + if (next_tag.fields.has("groups")) { + Array groups = next_tag.fields["groups"]; + for (const Variant &group : groups) { + packed_scene->get_state()->add_node_group(override_id, packed_scene->get_state()->add_name(group)); + } + } + + while (true) { + String assign; + Variant value; + + error = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, &parser); + + if (error) { + if (error == ERR_FILE_MISSING_DEPENDENCIES) { + // Resource loading error, just skip it. + } else if (error != ERR_FILE_EOF) { + ERR_FAIL_V_MSG(Ref(), vformat("Parse Error: %s. [Resource file %s:%d]", error_names[error], res_path, lines)); + } else { + error = OK; + return packed_scene; + } + } + + if (!assign.is_empty()) { + StringName assign_name = assign; + int nameidx = packed_scene->get_state()->add_name(assign_name); + int valueidx = packed_scene->get_state()->add_value(value); + packed_scene->get_state()->add_override_property(override_id, nameidx, valueidx, path_properties.has(assign_name)); + } else if (!next_tag.name.is_empty()) { + break; + } + } } else { error = ERR_FILE_CORRUPT; _printerr(); @@ -2047,6 +2086,48 @@ Error ResourceFormatSaverTextInstance::save(const String &p_path, const Refstore_line("[editable path=\"" + editable_instances[i].operator String().c_escape() + "\"]"); } + + // Store Overrides. + for (int i = 0; i < state->get_override_count(); i++) { + if (i == 0) { + f->store_line(""); + } + String header = "[override"; + NodePath path = state->get_override_path(i); + Vector groups = state->get_override_groups(i); + if (path != NodePath()) { + header += " path=\"" + String(path.simplified()).c_escape() + "\""; + } + if (!groups.is_empty()) { + // Write all groups on the same line as they're part of a section header. + // This improves readability while not impacting VCS friendliness too much, + // since it's rare to have more than 5 groups assigned to a single node. + groups.sort_custom(); + String sgroups = " groups=["; + for (int j = 0; j < groups.size(); j++) { + sgroups += "\"" + String(groups[j]).c_escape() + "\""; + if (j < groups.size() - 1) { + sgroups += ", "; + } + } + sgroups += "]"; + header += sgroups; + } + + f->store_string(header); + f->store_line("]"); + + for (int j = 0; j < state->get_override_node_property_count(i); j++) { + String vars; + VariantWriter::write_to_string(state->get_override_property_value(i, j), vars, _write_resources, this, use_compat); + + f->store_string(String(state->get_override_property_name(i, j)).property_name_encode() + " = " + vars + "\n"); + } + + if (i < state->get_override_count() - 1) { + f->store_line(String()); + } + } } if (f->get_error() != OK && f->get_error() != ERR_FILE_EOF) {