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) {