From 64f0c42e76a420d74647e04bfeab011515b351ff Mon Sep 17 00:00:00 2001 From: Juan Linietsky Date: Thu, 4 Jan 2024 13:30:47 +0100 Subject: [PATCH] Implement Resource Inheritance * Ability to inherit a resource and do local changes to it. * Ability to customize in each resource how inheritance happens. --- core/io/resource.cpp | 98 ++++++++++++++++++++++++ core/io/resource.h | 15 ++++ core/io/resource_format_binary.cpp | 4 +- doc/classes/EditorSettings.xml | 2 + doc/classes/Resource.xml | 24 ++++++ editor/editor_resource_picker.cpp | 16 ++++ editor/editor_resource_picker.h | 1 + scene/resources/resource_format_text.cpp | 4 +- 8 files changed, 158 insertions(+), 6 deletions(-) diff --git a/core/io/resource.cpp b/core/io/resource.cpp index 04ecabaf27b2..0257ba4512a3 100644 --- a/core/io/resource.cpp +++ b/core/io/resource.cpp @@ -437,6 +437,98 @@ String Resource::get_id_for_path(const String &p_path) const { } #endif +bool Resource::_property_can_revert(const StringName &p_name) const { + if (!inherits_state.is_valid()) { + return false; + } + bool value_valid; + Variant value = get(p_name, &value_valid); + if (!value_valid) { + return false; + } + Variant inherits_value = inherits_state->get(p_name, &value_valid); + if (!value_valid) { + return false; + } + return bool(Variant::evaluate(Variant::OP_NOT_EQUAL, value, inherits_value)); +} + +bool Resource::_property_get_revert(const StringName &p_name, Variant &r_property) const { + if (!inherits_state.is_valid()) { + return false; + } + + bool value_valid; + Variant inherits_value = inherits_state->get(p_name, &value_valid); + if (value_valid) { + r_property = inherits_value; + } + return value_valid; +} + +void Resource::_validate_property(PropertyInfo &p_property) const { + if (p_property.name == "resource_inherits") { + p_property.hint_string = get_class(); + } +} + +bool Resource::setup_inherits_state(const Ref &p_resource) { + bool ret; + if (GDVIRTUAL_CALL(_setup_inherits_state, p_resource, ret)) { + return ret; + } + + ERR_FAIL_COND_V_MSG(p_resource.is_valid() && !is_class(p_resource->get_class_name()), false, "Resources must be of (or class inherit) the same class when setting up state inheritance"); + + if (p_resource.is_valid()) { + copy_from(p_resource); + } + return true; +} + +void Resource::set_inherits_state(const Ref &p_resource) { + if (setup_inherits_state(p_resource)) { + inherits_state = p_resource; + } +} + +Ref Resource::get_inherits_state() const { + return inherits_state; +} + +bool Resource::is_inherited_state_property_value_saved(const StringName &p_name, const Variant &p_value) const { + bool ret; + if (GDVIRTUAL_CALL(_is_inherited_state_property_value_saved, p_name, p_value, ret)) { + return ret; + } + + bool default_value_valid; + Variant default_value = inherits_state->get(p_name, &default_value_valid); + if (!default_value_valid) { + return true; // Not a default value, so save. + } + + if (default_value.get_type() != Variant::NIL && bool(Variant::evaluate(Variant::OP_EQUAL, p_value, default_value))) { + return false; + } + + return true; +} + +bool Resource::is_property_value_saved(const StringName &p_property, const Variant &p_value) const { + if (!inherits_state.is_valid()) { + Variant default_value = ClassDB::class_get_default_property_value(get_class(), p_property); + + if (default_value.get_type() != Variant::NIL && bool(Variant::evaluate(Variant::OP_EQUAL, p_value, default_value))) { + return false; + } + + return true; + } else { + return is_inherited_state_property_value_saved(p_property, p_value); + } +} + void Resource::_bind_methods() { ClassDB::bind_method(D_METHOD("set_path", "path"), &Resource::_set_path); ClassDB::bind_method(D_METHOD("take_over_path", "path"), &Resource::_take_over_path); @@ -448,6 +540,9 @@ void Resource::_bind_methods() { ClassDB::bind_method(D_METHOD("is_local_to_scene"), &Resource::is_local_to_scene); ClassDB::bind_method(D_METHOD("get_local_scene"), &Resource::get_local_scene); ClassDB::bind_method(D_METHOD("setup_local_to_scene"), &Resource::setup_local_to_scene); + ClassDB::bind_method(D_METHOD("set_inherits_state", "base"), &Resource::set_inherits_state); + ClassDB::bind_method(D_METHOD("get_inherits_state"), &Resource::get_inherits_state); + ClassDB::bind_method(D_METHOD("is_property_value_saved", "property", "value"), &Resource::is_property_value_saved); ClassDB::bind_method(D_METHOD("emit_changed"), &Resource::emit_changed); @@ -459,12 +554,15 @@ void Resource::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "resource_local_to_scene"), "set_local_to_scene", "is_local_to_scene"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "resource_path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_path", "get_path"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "resource_name"), "set_name", "get_name"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "resource_inherits_state", PROPERTY_HINT_RESOURCE_TYPE, "Resource"), "set_inherits_state", "get_inherits_state"); MethodInfo get_rid_bind("_get_rid"); get_rid_bind.return_val.type = Variant::RID; ::ClassDB::add_virtual_method(get_class_static(), get_rid_bind, true, Vector(), true); GDVIRTUAL_BIND(_setup_local_to_scene); + GDVIRTUAL_BIND(_setup_inherits_state, "resource"); + GDVIRTUAL_BIND(_is_inherited_state_property_value_saved, "property", "value"); } Resource::Resource() : diff --git a/core/io/resource.h b/core/io/resource.h index 610c2150dbaa..bf0f4667c888 100644 --- a/core/io/resource.h +++ b/core/io/resource.h @@ -62,6 +62,8 @@ class Resource : public RefCounted { String path_cache; String scene_unique_id; + Ref inherits_state; + #ifdef TOOLS_ENABLED uint64_t last_modified_time = 0; uint64_t import_last_modified_time = 0; @@ -83,6 +85,15 @@ class Resource : public RefCounted { virtual void reset_local_to_scene(); GDVIRTUAL0(_setup_local_to_scene); + GDVIRTUAL1R(bool, _setup_inherits_state, Ref); + GDVIRTUAL2RC(bool, _is_inherited_state_property_value_saved, const StringName &, const Variant &); + + bool _property_can_revert(const StringName &p_name) const; + bool _property_get_revert(const StringName &p_name, Variant &r_property) const; + void _validate_property(PropertyInfo &p_property) const; + + virtual bool is_inherited_state_property_value_saved(const StringName &p_name, const Variant &p_value) const; + virtual bool setup_inherits_state(const Ref &p_resource); public: static Node *(*_get_local_scene_func)(); //used by editor @@ -145,6 +156,10 @@ class Resource : public RefCounted { String get_id_for_path(const String &p_path) const; #endif + void set_inherits_state(const Ref &p_resource); + Ref get_inherits_state() const; + bool is_property_value_saved(const StringName &p_property, const Variant &p_value) const; + Resource(); ~Resource(); }; diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp index 20c494516bee..01a93776553a 100644 --- a/core/io/resource_format_binary.cpp +++ b/core/io/resource_format_binary.cpp @@ -2189,9 +2189,7 @@ Error ResourceFormatSaverBinaryInstance::save(const String &p_path, const Refget_class(), F.name); - - if (default_value.get_type() != Variant::NIL && bool(Variant::evaluate(Variant::OP_EQUAL, p.value, default_value))) { + if (!E->is_property_value_saved(F.name, p.value)) { continue; } diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 5c317a50882c..9cc7f6a7da33 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -738,6 +738,8 @@ The sorting order to use in the project manager. When changing the sorting order in the project manager, this setting is set permanently in the editor settings. + + If [code]true[/code], saves all scenes and scripts automatically before running the project. Setting this to [code]false[/code] prevents the editor from saving if there are no changes which can speed up the project startup slightly, but it makes it possible to run a project that has unsaved changes. (Unsaved changes will not be visible in the running project.) diff --git a/doc/classes/Resource.xml b/doc/classes/Resource.xml index 49db3f81b448..08051cf224af 100644 --- a/doc/classes/Resource.xml +++ b/doc/classes/Resource.xml @@ -20,6 +20,20 @@ Override this method to return a custom [RID] when [method get_rid] is called. + + + + + + + + + + + + This function is called when resource state inheritance is being set up. The new base resource is notified via [b]resource[/b] while the existing one can still be obtained via the [member resource_inherits_state] property. + + @@ -71,6 +85,13 @@ Returns the [RID] of this resource (or an empty RID). Many resources (such as [Texture2D], [Mesh], and so on) are high-level abstractions of resources stored in a specialized server ([DisplayServer], [RenderingServer], etc.), so this function will return the original [RID]. + + + + + + + @@ -87,6 +108,9 @@ + + Resource that this resource inherits state from. It must be the same type as this one. When resource state inheritance is used, this resource will inherit the state of properties from the base one and only local notifications vs the base ones will be saved. + If [code]true[/code], the resource is duplicated for each instance of all scenes using it. At run-time, the resource can be modified in one scene without affecting other instances (see [method PackedScene.instantiate]). [b]Note:[/b] Changing this property at run-time has no effect on already created duplicate resources. diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index ed20e50685d2..870553b9aae0 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -217,6 +217,9 @@ void EditorResourcePicker::_update_menu_items() { if (is_editable()) { edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Clear")), TTR("Clear"), OBJ_MENU_CLEAR); edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate")), TTR("Make Unique"), OBJ_MENU_MAKE_UNIQUE); + if (edited_resource->get_inherits_state().is_null()) { + edit_menu->add_icon_item(get_editor_theme_icon(SNAME("Instance")), TTR("Inherit State"), OBJ_MENU_MAKE_INHERITED); + } // Check whether the resource has subresources. List property_list; @@ -362,7 +365,20 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { emit_signal(SNAME("resource_changed"), edited_resource); _update_resource(); } break; + case OBJ_MENU_MAKE_INHERITED: { + if (edited_resource.is_null()) { + return; + } + + Ref r = static_cast(ClassDB::instantiate(edited_resource->get_class())); + ERR_FAIL_COND(r.is_null()); + r->set_inherits_state(edited_resource); + + edited_resource = r; + emit_signal(SNAME("resource_changed"), edited_resource); + _update_resource(); + } break; case OBJ_MENU_MAKE_UNIQUE_RECURSIVE: { if (edited_resource.is_null()) { return; diff --git a/editor/editor_resource_picker.h b/editor/editor_resource_picker.h index 35703bcbeb64..f10325fd289a 100644 --- a/editor/editor_resource_picker.h +++ b/editor/editor_resource_picker.h @@ -71,6 +71,7 @@ class EditorResourcePicker : public HBoxContainer { OBJ_MENU_CLEAR, OBJ_MENU_MAKE_UNIQUE, OBJ_MENU_MAKE_UNIQUE_RECURSIVE, + OBJ_MENU_MAKE_INHERITED, OBJ_MENU_SAVE, OBJ_MENU_SAVE_AS, OBJ_MENU_COPY, diff --git a/scene/resources/resource_format_text.cpp b/scene/resources/resource_format_text.cpp index 29cd9f648dd0..d1ca10a12954 100644 --- a/scene/resources/resource_format_text.cpp +++ b/scene/resources/resource_format_text.cpp @@ -2183,9 +2183,7 @@ Error ResourceFormatSaverTextInstance::save(const String &p_path, const Refget_class(), name); - - if (default_value.get_type() != Variant::NIL && bool(Variant::evaluate(Variant::OP_EQUAL, value, default_value))) { + if (!res->is_property_value_saved(name, value)) { continue; }