diff --git a/core/variant/dictionary.cpp b/core/variant/dictionary.cpp
index 5f2ff1219bb..ed6693daa38 100644
--- a/core/variant/dictionary.cpp
+++ b/core/variant/dictionary.cpp
@@ -654,6 +654,10 @@ bool Dictionary::is_typed_value() const {
return _p->typed_value.type != Variant::NIL;
}
+bool Dictionary::is_same_instance(const Dictionary &p_other) const {
+ return _p == p_other._p;
+}
+
bool Dictionary::is_same_typed(const Dictionary &p_other) const {
return is_same_typed_key(p_other) && is_same_typed_value(p_other);
}
diff --git a/core/variant/dictionary.h b/core/variant/dictionary.h
index 45005d2b2b8..86c50b1c037 100644
--- a/core/variant/dictionary.h
+++ b/core/variant/dictionary.h
@@ -108,6 +108,7 @@ public:
bool is_typed() const;
bool is_typed_key() const;
bool is_typed_value() const;
+ bool is_same_instance(const Dictionary &p_other) const;
bool is_same_typed(const Dictionary &p_other) const;
bool is_same_typed_key(const Dictionary &p_other) const;
bool is_same_typed_value(const Dictionary &p_other) const;
diff --git a/doc/classes/Array.xml b/doc/classes/Array.xml
index d21bd3ac186..35415b3b3ac 100644
--- a/doc/classes/Array.xml
+++ b/doc/classes/Array.xml
@@ -332,7 +332,7 @@
Returns a new copy of the array.
- By default, a [b]shallow[/b] copy is returned: all nested [Array], [Dictionary], and [Resource] elements are shared with the original array. Modifying them in one array will also affect them in the other.
+ By default, a [b]shallow[/b] copy is returned: all nested [Array], [Dictionary], and [Resource] elements are shared with the original array. Modifying any of those in one array will also affect them in the other.
If [param deep] is [code]true[/code], a [b]deep[/b] copy is returned: all nested arrays and dictionaries are also duplicated (recursively). Any [Resource] is still shared with the original array, though.
diff --git a/doc/classes/Dictionary.xml b/doc/classes/Dictionary.xml
index 031ad415e75..efaeefd151b 100644
--- a/doc/classes/Dictionary.xml
+++ b/doc/classes/Dictionary.xml
@@ -188,7 +188,7 @@
Returns a new copy of the dictionary.
- By default, a [b]shallow[/b] copy is returned: all nested [Array], [Dictionary], and [Resource] keys and values are shared with the original dictionary. Modifying them in one dictionary will also affect them in the other.
+ By default, a [b]shallow[/b] copy is returned: all nested [Array], [Dictionary], and [Resource] keys and values are shared with the original dictionary. Modifying any of those in one dictionary will also affect them in the other.
If [param deep] is [code]true[/code], a [b]deep[/b] copy is returned: all nested arrays and dictionaries are also duplicated (recursively). Any [Resource] is still shared with the original dictionary, though.
diff --git a/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.gd b/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.gd
new file mode 100644
index 00000000000..507797d6cda
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.gd
@@ -0,0 +1,40 @@
+# We could use @export_custom to really test every property usage, but we know for good
+# that duplicating scripted properties flows through the same code already thoroughly tested
+# in the [Resource] test cases. The same goes for all the potential deep duplicate modes.
+# Therefore, it's enough to ensure the exported scriped properties are copied when invoking
+# duplication by each entry point.
+class TestResource:
+ extends Resource
+ @export var text: String = "holaaa"
+ @export var arr: Array = [1, 2, 3]
+ @export var dict: Dictionary = { "a": 1, "b": 2 }
+
+func test():
+ # Via Resource type.
+ var res := TestResource.new()
+ var dupe: TestResource
+
+ dupe = res.duplicate()
+ print(dupe.text)
+ print(dupe.arr)
+ print(dupe.dict)
+
+ dupe = res.duplicate_deep()
+ print(dupe.text)
+ print(dupe.arr)
+ print(dupe.dict)
+
+ # Via Variant type.
+
+ var res_var = TestResource.new()
+ var dupe_var
+
+ dupe_var = res_var.duplicate()
+ print(dupe_var.text)
+ print(dupe_var.arr)
+ print(dupe_var.dict)
+
+ dupe_var = res_var.duplicate_deep()
+ print(dupe_var.text)
+ print(dupe_var.arr)
+ print(dupe_var.dict)
diff --git a/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.out b/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.out
new file mode 100644
index 00000000000..a8e8dc2f9d7
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/duplicate_resource.out
@@ -0,0 +1,13 @@
+GDTEST_OK
+holaaa
+[1, 2, 3]
+{ "a": 1, "b": 2 }
+holaaa
+[1, 2, 3]
+{ "a": 1, "b": 2 }
+holaaa
+[1, 2, 3]
+{ "a": 1, "b": 2 }
+holaaa
+[1, 2, 3]
+{ "a": 1, "b": 2 }
diff --git a/tests/core/io/test_resource.h b/tests/core/io/test_resource.h
index ef98a8c0138..83b21762cef 100644
--- a/tests/core/io/test_resource.h
+++ b/tests/core/io/test_resource.h
@@ -34,37 +34,505 @@
#include "core/io/resource_loader.h"
#include "core/io/resource_saver.h"
#include "core/os/os.h"
+#include "scene/main/node.h"
#include "thirdparty/doctest/doctest.h"
#include "tests/test_macros.h"
+#include
+
namespace TestResource {
+enum TestDuplicateMode {
+ TEST_MODE_RESOURCE_DUPLICATE_SHALLOW,
+ TEST_MODE_RESOURCE_DUPLICATE_DEEP,
+ TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE,
+ TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE,
+ TEST_MODE_VARIANT_DUPLICATE_SHALLOW,
+ TEST_MODE_VARIANT_DUPLICATE_DEEP,
+ TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE,
+};
+
+class DuplicateGuineaPigData : public Object {
+ GDSOFTCLASS(DuplicateGuineaPigData, Object)
+
+public:
+ const Variant SENTINEL_1 = "A";
+ const Variant SENTINEL_2 = 645;
+ const Variant SENTINEL_3 = StringName("X");
+ const Variant SENTINEL_4 = true;
+
+ Ref SUBRES_1 = memnew(Resource);
+ Ref SUBRES_2 = memnew(Resource);
+ Ref SUBRES_3 = memnew(Resource);
+ Ref SUBRES_SL_1 = memnew(Resource);
+ Ref SUBRES_SL_2 = memnew(Resource);
+ Ref SUBRES_SL_3 = memnew(Resource);
+
+ Variant obj; // Variant helps with lifetime so duplicates pointing to the same don't try to double-free it.
+ Array arr;
+ Dictionary dict;
+ Variant packed; // A PackedByteArray, but using Variant to be able to tell if the array is shared or not.
+ Ref subres;
+ Ref subres_sl;
+
+ void set_defaults() {
+ SUBRES_1->set_name("juan");
+ SUBRES_2->set_name("you");
+ SUBRES_3->set_name("tree");
+ SUBRES_SL_1->set_name("maybe_scene_local");
+ SUBRES_SL_2->set_name("perhaps_local_to_scene");
+ SUBRES_SL_3->set_name("sometimes_locality_scenial");
+
+ // To try some cases of internal and external.
+ SUBRES_1->set_path_cache("");
+ SUBRES_2->set_path_cache("local://hehe");
+ SUBRES_3->set_path_cache("res://some.tscn::1");
+ DEV_ASSERT(SUBRES_1->is_built_in());
+ DEV_ASSERT(SUBRES_2->is_built_in());
+ DEV_ASSERT(SUBRES_3->is_built_in());
+ SUBRES_SL_1->set_path_cache("res://thing.scn");
+ SUBRES_SL_2->set_path_cache("C:/not/really/possible/but/still/external");
+ SUBRES_SL_3->set_path_cache("/this/neither");
+ DEV_ASSERT(!SUBRES_SL_1->is_built_in());
+ DEV_ASSERT(!SUBRES_SL_2->is_built_in());
+ DEV_ASSERT(!SUBRES_SL_3->is_built_in());
+
+ obj = memnew(Object);
+
+ // Construct enough cases to test deep recursion involving resources;
+ // we mix some primitive values with recurses nested in different ways,
+ // acting as array values and dictionary keys and values, some of those
+ // being marked as scene-local when for subcases where scene-local is relevant.
+
+ arr.push_back(SENTINEL_1);
+ arr.push_back(SUBRES_1);
+ arr.push_back(SUBRES_SL_1);
+ {
+ Dictionary d;
+ d[SENTINEL_2] = SENTINEL_3;
+ d[SENTINEL_4] = SUBRES_2;
+ d[SUBRES_3] = SUBRES_SL_2;
+ d[SUBRES_SL_3] = SUBRES_1;
+ arr.push_back(d);
+ }
+
+ dict[SENTINEL_4] = SENTINEL_1;
+ dict[SENTINEL_2] = SUBRES_2;
+ dict[SUBRES_3] = SUBRES_SL_1;
+ dict[SUBRES_SL_2] = SUBRES_1;
+ {
+ Array a;
+ a.push_back(SENTINEL_3);
+ a.push_back(SUBRES_2);
+ a.push_back(SUBRES_SL_3);
+ dict[SENTINEL_4] = a;
+ }
+
+ packed = PackedByteArray{ 0xaa, 0xbb, 0xcc };
+
+ subres = SUBRES_1;
+ subres_sl = SUBRES_SL_1;
+ }
+
+ void verify_empty() const {
+ CHECK(obj.get_type() == Variant::NIL);
+ CHECK(arr.size() == 0);
+ CHECK(dict.size() == 0);
+ CHECK(packed.get_type() == Variant::NIL);
+ CHECK(subres.is_null());
+ }
+
+ void verify_duplication(const DuplicateGuineaPigData *p_orig, uint32_t p_property_usage, TestDuplicateMode p_test_mode, ResourceDeepDuplicateMode p_deep_mode) const {
+ if (!(p_property_usage & PROPERTY_USAGE_STORAGE)) {
+ verify_empty();
+ return;
+ }
+
+ // To see if each resource involved is copied once at most,
+ // and then the reference to the duplicate reused.
+ HashMap duplicates;
+
+ auto _verify_resource = [&](const Ref &p_dupe_res, const Ref &p_orig_res, bool p_is_property = false) {
+ bool expect_true_copy = (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP && p_orig_res->is_built_in()) ||
+ (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL && p_orig_res->is_built_in()) ||
+ (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_ALL) ||
+ (p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE && p_orig_res->is_local_to_scene()) ||
+ (p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL && p_orig_res->is_built_in()) ||
+ (p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE && p_deep_mode == RESOURCE_DEEP_DUPLICATE_ALL);
+
+ if (expect_true_copy) {
+ if (p_deep_mode == RESOURCE_DEEP_DUPLICATE_NONE) {
+ expect_true_copy = false;
+ } else if (p_deep_mode == RESOURCE_DEEP_DUPLICATE_INTERNAL) {
+ expect_true_copy = p_orig_res->is_built_in();
+ }
+ }
+
+ if (p_is_property) {
+ if ((p_property_usage & PROPERTY_USAGE_ALWAYS_DUPLICATE)) {
+ expect_true_copy = true;
+ } else if ((p_property_usage & PROPERTY_USAGE_NEVER_DUPLICATE)) {
+ expect_true_copy = false;
+ }
+ }
+
+ if (expect_true_copy) {
+ CHECK(p_dupe_res != p_orig_res);
+ CHECK(p_dupe_res->get_name() == p_orig_res->get_name());
+ if (duplicates.has(p_orig_res.ptr())) {
+ CHECK(duplicates[p_orig_res.ptr()] == p_dupe_res.ptr());
+ } else {
+ duplicates[p_orig_res.ptr()] = p_dupe_res.ptr();
+ }
+ } else {
+ CHECK(p_dupe_res == p_orig_res);
+ }
+ };
+
+ std::function _verify_deep_copied_variants = [&](const Variant &p_a, const Variant &p_b) {
+ CHECK(p_a.get_type() == p_b.get_type());
+ const Ref &res_a = p_a;
+ const Ref &res_b = p_b;
+ if (res_a.is_valid()) {
+ _verify_resource(res_a, res_b);
+ } else if (p_a.get_type() == Variant::ARRAY) {
+ const Array &arr_a = p_a;
+ const Array &arr_b = p_b;
+ CHECK(!arr_a.is_same_instance(arr_b));
+ CHECK(arr_a.size() == arr_b.size());
+ for (int i = 0; i < arr_a.size(); i++) {
+ _verify_deep_copied_variants(arr_a[i], arr_b[i]);
+ }
+ } else if (p_a.get_type() == Variant::DICTIONARY) {
+ const Dictionary &dict_a = p_a;
+ const Dictionary &dict_b = p_b;
+ CHECK(!dict_a.is_same_instance(dict_b));
+ CHECK(dict_a.size() == dict_b.size());
+ for (int i = 0; i < dict_a.size(); i++) {
+ _verify_deep_copied_variants(dict_a.get_key_at_index(i), dict_b.get_key_at_index(i));
+ _verify_deep_copied_variants(dict_a.get_value_at_index(i), dict_b.get_value_at_index(i));
+ }
+ } else {
+ CHECK(p_a == p_b);
+ }
+ };
+
+ CHECK(this != p_orig);
+
+ CHECK((Object *)obj == (Object *)p_orig->obj);
+
+ bool expect_true_copy = p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP ||
+ p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE ||
+ p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE ||
+ p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP ||
+ p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE;
+ if (expect_true_copy) {
+ _verify_deep_copied_variants(arr, p_orig->arr);
+ _verify_deep_copied_variants(dict, p_orig->dict);
+ CHECK(!packed.identity_compare(p_orig->packed));
+ } else {
+ CHECK(arr.is_same_instance(p_orig->arr));
+ CHECK(dict.is_same_instance(p_orig->dict));
+ CHECK(packed.identity_compare(p_orig->packed));
+ }
+
+ _verify_resource(subres, p_orig->subres, true);
+ _verify_resource(subres_sl, p_orig->subres_sl, true);
+ }
+
+ void enable_scene_local_subresources() {
+ SUBRES_SL_1->set_local_to_scene(true);
+ SUBRES_SL_2->set_local_to_scene(true);
+ SUBRES_SL_3->set_local_to_scene(true);
+ }
+
+ virtual ~DuplicateGuineaPigData() {
+ Object *obj_ptr = obj.get_validated_object();
+ if (obj_ptr) {
+ memdelete(obj_ptr);
+ }
+ }
+};
+
+#define DEFINE_DUPLICATE_GUINEA_PIG(m_class_name, m_property_usage) \
+ class m_class_name : public Resource { \
+ GDCLASS(m_class_name, Resource) \
+ \
+ DuplicateGuineaPigData data; \
+ \
+ public: \
+ void set_obj(Object *p_obj) { \
+ data.obj = p_obj; \
+ } \
+ Object *get_obj() const { \
+ return data.obj; \
+ } \
+ \
+ void set_arr(const Array &p_arr) { \
+ data.arr = p_arr; \
+ } \
+ Array get_arr() const { \
+ return data.arr; \
+ } \
+ \
+ void set_dict(const Dictionary &p_dict) { \
+ data.dict = p_dict; \
+ } \
+ Dictionary get_dict() const { \
+ return data.dict; \
+ } \
+ \
+ void set_packed(const Variant &p_packed) { \
+ data.packed = p_packed; \
+ } \
+ Variant get_packed() const { \
+ return data.packed; \
+ } \
+ \
+ void set_subres(const Ref &p_subres) { \
+ data.subres = p_subres; \
+ } \
+ Ref get_subres() const { \
+ return data.subres; \
+ } \
+ \
+ void set_subres_sl(const Ref &p_subres) { \
+ data.subres_sl = p_subres; \
+ } \
+ Ref get_subres_sl() const { \
+ return data.subres_sl; \
+ } \
+ \
+ void set_defaults() { \
+ data.set_defaults(); \
+ } \
+ \
+ Object *get_data() { \
+ return &data; \
+ } \
+ \
+ void verify_duplication(const Ref &p_orig, int p_test_mode, int p_deep_mode) const { \
+ const DuplicateGuineaPigData *orig_data = Object::cast_to(p_orig->call("get_data")); \
+ data.verify_duplication(orig_data, m_property_usage, (TestDuplicateMode)p_test_mode, (ResourceDeepDuplicateMode)p_deep_mode); \
+ } \
+ \
+ protected: \
+ static void _bind_methods() { \
+ ClassDB::bind_method(D_METHOD("set_obj", "obj"), &m_class_name::set_obj); \
+ ClassDB::bind_method(D_METHOD("get_obj"), &m_class_name::get_obj); \
+ \
+ ClassDB::bind_method(D_METHOD("set_arr", "arr"), &m_class_name::set_arr); \
+ ClassDB::bind_method(D_METHOD("get_arr"), &m_class_name::get_arr); \
+ \
+ ClassDB::bind_method(D_METHOD("set_dict", "dict"), &m_class_name::set_dict); \
+ ClassDB::bind_method(D_METHOD("get_dict"), &m_class_name::get_dict); \
+ \
+ ClassDB::bind_method(D_METHOD("set_packed", "packed"), &m_class_name::set_packed); \
+ ClassDB::bind_method(D_METHOD("get_packed"), &m_class_name::get_packed); \
+ \
+ ClassDB::bind_method(D_METHOD("set_subres", "subres"), &m_class_name::set_subres); \
+ ClassDB::bind_method(D_METHOD("get_subres"), &m_class_name::get_subres); \
+ \
+ ClassDB::bind_method(D_METHOD("set_subres_sl", "subres"), &m_class_name::set_subres_sl); \
+ ClassDB::bind_method(D_METHOD("get_subres_sl"), &m_class_name::get_subres_sl); \
+ \
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "obj", PROPERTY_HINT_NONE, "", m_property_usage), "set_obj", "get_obj"); \
+ ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "arr", PROPERTY_HINT_NONE, "", m_property_usage), "set_arr", "get_arr"); \
+ ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "dict", PROPERTY_HINT_NONE, "", m_property_usage), "set_dict", "get_dict"); \
+ ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "packed", PROPERTY_HINT_NONE, "", m_property_usage), "set_packed", "get_packed"); \
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "subres", PROPERTY_HINT_NONE, "", m_property_usage), "set_subres", "get_subres"); \
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "subres_sl", PROPERTY_HINT_NONE, "", m_property_usage), "set_subres_sl", "get_subres_sl"); \
+ \
+ ClassDB::bind_method(D_METHOD("set_defaults"), &m_class_name::set_defaults); \
+ ClassDB::bind_method(D_METHOD("get_data"), &m_class_name::get_data); \
+ ClassDB::bind_method(D_METHOD("verify_duplication", "orig", "test_mode", "deep_mode"), &m_class_name::verify_duplication); \
+ } \
+ \
+ public: \
+ static m_class_name *register_and_instantiate() { \
+ static bool registered = false; \
+ if (!registered) { \
+ GDREGISTER_CLASS(m_class_name); \
+ registered = true; \
+ } \
+ return memnew(m_class_name); \
+ } \
+ };
+
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_None, PROPERTY_USAGE_NONE)
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Always, PROPERTY_USAGE_ALWAYS_DUPLICATE)
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage, PROPERTY_USAGE_STORAGE)
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage_Always, (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_ALWAYS_DUPLICATE))
+DEFINE_DUPLICATE_GUINEA_PIG(DuplicateGuineaPig_Storage_Never, (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_NEVER_DUPLICATE))
+
TEST_CASE("[Resource] Duplication") {
- Ref resource = memnew(Resource);
- resource->set_name("Hello world");
- Ref child_resource = memnew(Resource);
- child_resource->set_name("I'm a child resource");
- resource->set_meta("other_resource", child_resource);
+ auto _run_test = [](
+ TestDuplicateMode p_test_mode,
+ ResourceDeepDuplicateMode p_deep_mode,
+ Ref (*p_duplicate_fn)(const Ref &)) -> void {
+ LocalVector[> resources = {
+ DuplicateGuineaPig_None::register_and_instantiate(),
+ DuplicateGuineaPig_Always::register_and_instantiate(),
+ DuplicateGuineaPig_Storage::register_and_instantiate(),
+ DuplicateGuineaPig_Storage_Always::register_and_instantiate(),
+ DuplicateGuineaPig_Storage_Never::register_and_instantiate(),
+ };
- Ref resource_dupe = resource->duplicate();
- const Ref &resource_dupe_reference = resource_dupe;
- resource_dupe->set_name("Changed name");
- child_resource->set_name("My name was changed too");
+ for (const Ref &orig : resources) {
+ INFO(std::string(String(orig->get_class_name()).utf8().get_data()));
- CHECK_MESSAGE(
- resource_dupe->get_name() == "Changed name",
- "Duplicated resource should have the new name.");
- CHECK_MESSAGE(
- resource_dupe_reference->get_name() == "Changed name",
- "Reference to the duplicated resource should have the new name.");
- CHECK_MESSAGE(
- resource->get_name() == "Hello world",
- "Original resource name should not be affected after editing the duplicate's name.");
- CHECK_MESSAGE(
- Ref(resource_dupe->get_meta("other_resource"))->get_name() == "My name was changed too",
- "Duplicated resource should share its child resource with the original.");
+ orig->call("set_defaults");
+
+ const Ref &dupe = p_duplicate_fn(orig);
+ if ((p_test_mode == TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE || p_test_mode == TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE) && p_deep_mode == RESOURCE_DEEP_DUPLICATE_MAX) {
+ CHECK(dupe.is_null());
+ } else {
+ dupe->call("verify_duplication", orig, p_test_mode, p_deep_mode);
+ }
+ }
+ };
+
+ SUBCASE("Resource::duplicate(), shallow") {
+ _run_test(
+ TEST_MODE_RESOURCE_DUPLICATE_SHALLOW,
+ RESOURCE_DEEP_DUPLICATE_MAX,
+ [](const Ref &p_res) -> Ref {
+ return p_res->duplicate(false);
+ });
+ }
+
+ SUBCASE("Resource::duplicate(), deep") {
+ _run_test(
+ TEST_MODE_RESOURCE_DUPLICATE_DEEP,
+ RESOURCE_DEEP_DUPLICATE_MAX,
+ [](const Ref &p_res) -> Ref {
+ return p_res->duplicate(true);
+ });
+ }
+
+ SUBCASE("Resource::duplicate_deep()") {
+ static int deep_mode = 0;
+ for (deep_mode = 0; deep_mode <= RESOURCE_DEEP_DUPLICATE_MAX; deep_mode++) {
+ _run_test(
+ TEST_MODE_RESOURCE_DUPLICATE_DEEP_WITH_MODE,
+ (ResourceDeepDuplicateMode)deep_mode,
+ [](const Ref &p_res) -> Ref {
+ return p_res->duplicate_deep((ResourceDeepDuplicateMode)deep_mode);
+ });
+ }
+ }
+
+ SUBCASE("Resource::duplicate_for_local_scene()") {
+ static int mark_main_as_local = 0;
+ static int mark_some_subs_as_local = 0;
+ for (mark_main_as_local = 0; mark_main_as_local < 2; ++mark_main_as_local) { // Whether main is local-to-scene shouldn't matter.
+ for (mark_some_subs_as_local = 0; mark_some_subs_as_local < 2; ++mark_some_subs_as_local) {
+ _run_test(
+ TEST_MODE_RESOURCE_DUPLICATE_FOR_LOCAL_SCENE,
+ RESOURCE_DEEP_DUPLICATE_MAX,
+ [](const Ref &p_res) -> Ref {
+ if (mark_main_as_local) {
+ p_res->set_local_to_scene(true);
+ }
+ if (mark_some_subs_as_local) {
+ Object::cast_to(p_res->call("get_data"))->enable_scene_local_subresources();
+ }
+ HashMap][, Ref> remap_cache;
+ Node fake_scene;
+ return p_res->duplicate_for_local_scene(&fake_scene, remap_cache);
+ });
+ }
+ }
+ }
+
+ SUBCASE("Variant::duplicate(), shallow") {
+ _run_test(
+ TEST_MODE_VARIANT_DUPLICATE_SHALLOW,
+ RESOURCE_DEEP_DUPLICATE_MAX,
+ [](const Ref &p_res) -> Ref {
+ return Variant(p_res).duplicate(false);
+ });
+ }
+
+ SUBCASE("Variant::duplicate(), deep") {
+ _run_test(
+ TEST_MODE_VARIANT_DUPLICATE_DEEP,
+ RESOURCE_DEEP_DUPLICATE_MAX,
+ [](const Ref &p_res) -> Ref {
+ return Variant(p_res).duplicate(true);
+ });
+ }
+
+ SUBCASE("Variant::duplicate_deep()") {
+ static int deep_mode = 0;
+ for (deep_mode = 0; deep_mode <= RESOURCE_DEEP_DUPLICATE_MAX; deep_mode++) {
+ _run_test(
+ TEST_MODE_VARIANT_DUPLICATE_DEEP_WITH_MODE,
+ (ResourceDeepDuplicateMode)deep_mode,
+ [](const Ref &p_res) -> Ref {
+ return Variant(p_res).duplicate_deep((ResourceDeepDuplicateMode)deep_mode);
+ });
+ }
+ }
+
+ SUBCASE("Via Variant, resource not being the root") {
+ // Variant controls the deep copy, recursing until resources are found, and then
+ // it's Resource who controls the deep copy from it onwards.
+ // Therefore, we have to test if Variant is able to track unique duplicates across
+ // multiple times Resource takes over.
+ // Since the other test cases already prove Resource's mechanism to have at most
+ // one duplicate per resource involved, the test for Variant is simple.
+
+ Ref res;
+ res.instantiate();
+ res->set_name("risi");
+ Array a;
+ a.push_back(res);
+ {
+ Dictionary d;
+ d[res] = res;
+ a.push_back(d);
+ }
+
+ Array dupe_a;
+ Ref dupe_res;
+
+ SUBCASE("Variant::duplicate(), shallow") {
+ dupe_a = Variant(a).duplicate(false);
+ // Ensure it's referencing the original.
+ dupe_res = dupe_a[0];
+ CHECK(dupe_res == res);
+ }
+ SUBCASE("Variant::duplicate(), deep") {
+ dupe_a = Variant(a).duplicate(true);
+ // Ensure it's referencing the original.
+ dupe_res = dupe_a[0];
+ CHECK(dupe_res == res);
+ }
+ SUBCASE("Variant::duplicate_deep(), no resources") {
+ dupe_a = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_NONE);
+ // Ensure it's referencing the original.
+ dupe_res = dupe_a[0];
+ CHECK(dupe_res == res);
+ }
+ SUBCASE("Variant::duplicate_deep(), with resources") {
+ dupe_a = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_ALL);
+ // Ensure it's a copy.
+ dupe_res = dupe_a[0];
+ CHECK(dupe_res != res);
+ CHECK(dupe_res->get_name() == "risi");
+
+ // Ensure the map is already gone so we get new instances.
+ Array dupe_a_2 = Variant(a).duplicate_deep(RESOURCE_DEEP_DUPLICATE_ALL);
+ CHECK(dupe_a_2[0] != dupe_a[0]);
+ }
+
+ // Ensure all the usages are of the same resource.
+ CHECK(((Dictionary)dupe_a[1]).get_key_at_index(0) == dupe_res);
+ CHECK(((Dictionary)dupe_a[1]).get_value_at_index(0) == dupe_res);
+ }
}
TEST_CASE("[Resource] Saving and loading") {
]