godot icon indicating copy to clipboard operation
godot copied to clipboard

`Resource.duplicate(true)` doesn't duplicate subresources stored in Array or Dictionary properties

Open limbonaut opened this issue 2 years ago • 35 comments
trafficstars


Bugsquad note: This issue has been confirmed several times already. No need to confirm it further.


Godot version

4.0.stable

System information

Manjaro Linux

Issue description

This behavior is poorly documented, and it breaks expectations. It is mentioned in the docs only under Array.duplicate(), but not Dictionary class, or Resource.duplicate().

This affects also "Make Unique" and "Make Unique (Recursive)" for custom resources.

Here's the problematic code: https://github.com/godotengine/godot/blob/fc7adaab7b3856a7831d402ea2bbb27efe7b7d8a/core/variant/variant_setget.cpp#L1889-L1900

Steps to reproduce

For simple code see reproduction project.

To reproduce on your own:

  1. Have a resource with a property of type Array[SomeOtherResource]
  2. One of the following: 2a) "Make Unique" on parent resource in inspector. 2b) Or duplicate(true) from code.
  3. Check if SomeOtherResource items inside array are unique.

Minimal reproduction project

res_bug_reproduction.zip

limbonaut avatar Mar 14 '23 18:03 limbonaut

Thank you! I believe I experienced this with 4.0-stable trying to duplicate a custom resource with a dictionary that contained additional dictionaries. I ended up writing a function to create a new resource and copy over the data piece-by-piece, but never figured out why duplicate(true) hadn’t worked. Some additional documentation would save the next person a lot of time.

dandeliondino avatar Mar 15 '23 04:03 dandeliondino

It's troublesome that this isn't fixed yet, my game relies a lot on duplicating resources and arrays and I was really miffed when I just found out deep copying isn't functioning properly. This should be tracked and added to a milestone.

popcar2 avatar Jul 12 '23 23:07 popcar2

This also occurs on all 3.x variants.

stryker313 avatar Jul 28 '23 11:07 stryker313

I confim that my_resource = my_resource.duplicate(true) doesn't make unique sub-resources as the documentation says. Godot 4.1.

luislodosm avatar Sep 05 '23 17:09 luislodosm

I also met this situation in c#,I set the resourcelocaltoscene = true, and the resource members are not shared except the array and dictionary. this really affect,it can provide one override copy function for resource.

zhuyeaini9 avatar Sep 23 '23 05:09 zhuyeaini9

Does anyone know if this is being looked at or at least planned to be included in a future release?

A-Sandwich avatar Oct 08 '23 18:10 A-Sandwich

I hit this bug during a game jam (v4.1.2).

benfrankel avatar Nov 16 '23 02:11 benfrankel

I'm guessing one of the issues with this (though perhaps not the only reason it was removed) is because resources can self-reference which would break this. Here is a pseudo-code thought on it:


Variant Variant::recursive_duplicate(bool p_deep, int recursion_count, std::unordered_map<Ref<Resource>, Ref<Resource>> refs) const {
	switch (type) {
		case OBJECT: {
			Ref<Resource> resource = _get_obj().ref;
			if (p_deep && !resource.is_null() && resource.is_valid()) {
				if (refs.contains(resource))
					return refs[resource];
				Ref<Resource> resource_new = resource->duplicate(true, refs);
				refs[resource] = resource_new;
				return resource_new;
			}
			return *this;
		} break;

This isn't in a working state but I am trying to look at what direction it might be able to go and help the conversation around this as I need this fixed myself.

DDarby-Lewis avatar Nov 29 '23 21:11 DDarby-Lewis

Oh my god, I was confused why all my enemy mobs die at the same time like they have shared HP. Had to make multiple loops to manually duplicate subresources in my ready function to work around this. This is rough for stat heavy games out there.

frozenwood avatar Dec 31 '23 12:12 frozenwood

I came across this issues when duplicating CollisionShape2D in the Scene... but i guess this is using the same function beneath

Yinimi avatar Jan 04 '24 23:01 Yinimi

~~This seems to have been fixed. Using Resource.duplicate(true) duplicates the subresources as well in v4.2.1~~

Edit: This is not true for Array[Resource], though which is the intent of the issue. My bad!

henrypickler avatar Feb 06 '24 16:02 henrypickler

This seems to have been fixed. Using Resource.duplicate(true) duplicates the subresources as well in v4.2.1

I've tested this recently in 4.2.1 and no, it wasn't fixed when using Array at least. If you think it's fixed, please post a simple example of it working, as it might help solve the issue.

dfego avatar Feb 06 '24 16:02 dfego

I can't believe this hasn't been fixed, my project relies heavily on resources that have arrays or dictionaries in them, for localToScene to not work with them is really bad. I'm now having to write duplication methods for more than 10 different classes.

GalaxasaurusGames avatar Feb 12 '24 18:02 GalaxasaurusGames

Just hit this bug as well. The most frustrating part is that, as the original post already statet, the documentation does (still ?!?) not mention it in the most crucial places. Now I will have to retroactively add custom duplicate functions to most of my classes. Big thanks to anyone who is able to fix this.

T4n4t0s avatar Feb 16 '24 16:02 T4n4t0s

Please don't bump without contributing significant new information. Use the :+1: reaction button on the first post instead.

AThousandShips avatar Feb 16 '24 16:02 AThousandShips

Can this at least be updated in the documentation ASAP even if it won't be fixed any time soon? The Resource.duplicate docs confidently state:

If subresources is true, a deep copy is returned; nested subresources will be duplicated and are not shared.

Which is clearly not true and causing headaches for many users in this thread. It's now been over 1 year with no comments from the team.

devonveldhuis avatar Mar 21 '24 10:03 devonveldhuis

it's not fix now. has any way to fix this problem?

I made a gdscript class, it's extend Resource to implement a function call _duplicate to fix this problem.

But please fix this in the engine as soon as possible.

the class code is :

class_name BaseResource

extends Resource


func _duplicate(flag):
	var newRes = self.duplicate(flag)
	if flag:
		newRes._CheckDuplicateRes()
	return newRes

func _ArrayCheck(type,pvalue):
	var needcpobj = {}
	for i in range(pvalue.size()):
		var value = pvalue[i]
		var vtype = typeof(value)
		#handle type obj copy
		if vtype == TYPE_OBJECT:
			if not value.has_method("_duplicate"): continue
			var dpvalue = value._duplicate(true)
			needcpobj[i] = dpvalue
		else:
			self._CheckType(vtype,value)

	for i in needcpobj.keys():
		pvalue[i] = needcpobj[i]
	return

func _DictionaryCheck(type,pvalue):
	var needcpobj = {}
	for key in pvalue:
		var value = pvalue[key]
		var vtype = typeof(value)
		#handle type obj copy
		if vtype == TYPE_OBJECT:
			if not value.has_method("_duplicate"): continue
			var dpvalue = value._duplicate(true)
			needcpobj[key] = dpvalue
		else:
			self._CheckType(vtype,value)

	for i in needcpobj.keys():
		pvalue[i] = needcpobj[i]
	return

func _CheckType(type,value):
	match type:
		TYPE_ARRAY:
			_ArrayCheck(type,value)
			return
		TYPE_DICTIONARY:
			_DictionaryCheck(type,value)
			return
		TYPE_OBJECT:
			if value.has_method("_CheckDuplicateRes"):
				value._CheckDuplicateRes()
			return

func _CheckDuplicateRes():
	for property in self.get_property_list():
		if property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
			var value = self.get(property.name)
			if value == null: continue
			var type = property.type
			self._CheckType(type,value)
	return

595145489 avatar Apr 09 '24 13:04 595145489

it's not fix now. has any way to fix this problem?

I made a gdscript class, it's extend Resource to implement a function call _duplicate to fix this problem.

But please fix this in the engine as soon as possible.

the class code is :

class_name BaseResource

Edit: So, I kinda figured it out where my (and many others) issues come from regarding duplicating resources and saving and loading them.

The initial problem is that saving and loading subresources just does not work if you simply save and load using ResourceSaver and RexourceLoader IF any of your subresources are actually instanced. ResourceSaver will just save a reference to the project file instance.

Now, the "FLAG_BUNDLE_RESOURCES " should fix it, but it is actually not working right now (#65393)

If you want to save your instanced subresources you need to deep duplicate them using "BaseResource".

It's also important du make deep duplications when you have a resource "template" saved that you later want to add to an array List multiple times.


I am trying to use this, together with: https://github.com/godotengine/godot/issues/65393#issuecomment-1847601360 to save and load resources that contain an Array with other Resources.

It does not work unfortunately:

E 0:00:09:0688   SaveManager.gd:17 @ loadAll(): Attempted to assign an object into a TypedArray, that does not inherit from 'GDScript'.
  <C++ Error>    Condition "!other_script->inherits_script(script)" is true. Returning: false
  <C++ Source>   core/variant/container_type_validate.h:140 @ validate_object()
  <Stack Trace>  SaveManager.gd:17 @ loadAll()
                 SaveManager.gd:9 @ _init()
		var loadfile = ResourceLoader.load(SAVEDIR,"GameStats", 0)

E 0:00:09:0688   SaveManager.gd:17 @ loadAll(): unsupported format character
  <C++ Error>    Condition "error" is true. Returning: String()
  <C++ Source>   ./core/variant/variant.h:834 @ vformat()
  <Stack Trace>  SaveManager.gd:17 @ loadAll()
                 SaveManager.gd:9 @ _init()

E 0:00:09:0688   SaveManager.gd:17 @ loadAll(): Method/function failed.
  <C++ Source>   core/variant/array.cpp:222 @ assign()
  <Stack Trace>  SaveManager.gd:17 @ loadAll()
                 SaveManager.gd:9 @ _init()

E 0:00:09:0695   SaveManager.gd:17 @ loadAll(): Attempted to assign an object into a TypedArray, that does not inherit from 'GDScript'.
  <C++ Error>    Condition "!other_script->inherits_script(script)" is true. Returning: false
  <C++ Source>   core/variant/container_type_validate.h:140 @ validate_object()
  <Stack Trace>  SaveManager.gd:17 @ loadAll()
                 SaveManager.gd:9 @ _init()

E 0:00:09:0695   SaveManager.gd:17 @ loadAll(): unsupported format character
  <C++ Error>    Condition "error" is true. Returning: String()
  <C++ Source>   ./core/variant/variant.h:834 @ vformat()
  <Stack Trace>  SaveManager.gd:17 @ loadAll()
                 SaveManager.gd:9 @ _init()

E 0:00:09:0695   SaveManager.gd:17 @ loadAll(): Method/function failed.
  <C++ Source>   core/variant/array.cpp:222 @ assign()
  <Stack Trace>  SaveManager.gd:17 @ loadAll()
                 SaveManager.gd:9 @ _init()

Has anyone managed to save and load resources that contain an Array of another resources?

Saadies avatar Apr 14 '24 14:04 Saadies

Any news on this? It's pretty crucial for a resource heavy game :/

filipinyo avatar May 13 '24 09:05 filipinyo

Yeah this seems like a pretty major bug to me. It's workaroundable but I don't particuarly like the elegancy of any of the workarounds.

JonRoberts44 avatar May 14 '24 11:05 JonRoberts44

I'm not sure if this is related or a new bug, but I'm finding nothing is duplicated in my custom resource. I've got a custom resource with a bunch of different variables including dictionaries and standalone variable ints and such. ~~And when attempting to use the duplicate(true) function none of it is copied over. The dictionaries are blank and the integers are also their default value.~~ It seems like integers based on enums are not copied, but regular integers are.... strange.

But essentially I have to manually copy over every value from one resource to another, which is far from ideal.

LeifintheWind avatar Jun 11 '24 18:06 LeifintheWind

Also stumbled upon this issue after a day of frustration. The docs should really be updated as "If subresources is true, a deep copy is returned; nested subresources will be duplicated and are not shared." is factually not correct.

As nobody explicitly mentioned the simple (but dumb) workaround: after creating a duplicate using duplicate(true), you can iterate over the arrays and dictionaries in those resources (and the ones in those, if you have them nested) and replace the elements with duplicates of themselves. Like so:

func duplicate_deep_workaround() -> SomeCustomResource:
	var dup: SomeCustomResource = duplicate(true) as SomeCustomResource
	for i: int in dup.some_array.size():
		dup.some_array[i] = dup.some_array[i].duplicate(true)
	return dup

AdriaandeJongh avatar Jul 09 '24 14:07 AdriaandeJongh

@AdriaandeJongh that was actually already said though. It's a workaround but it doesn't actually ease the pain of nested sub resources 💔

Leggy7 avatar Jul 09 '24 14:07 Leggy7

It seems to me to be a larger bug with duplication functionality. If I (on 4.2.2, OSX) duplicate an item, then change any of its properties, the properties of the original are also changed. Doesn't matter what kind of node it is or what property it is. Is there a memory reference issue going on maybe?

briankellydev avatar Jul 12 '24 06:07 briankellydev

If I (on 4.2.2, OSX) duplicate an item

Are you duplicating a node or a resource? How are you duplicating it?

Duplicating a node does not automatically make its Resource-based properties (or their subresources) unique, with a handful of exceptions (properties marked with PROPERTY_USAGE_ALWAYS_DUPLICATE).

Calinou avatar Jul 12 '24 21:07 Calinou

If I (on 4.2.2, OSX) duplicate an item

Are you duplicating a node or a resource? How are you duplicating it?

Duplicating a node does not automatically make its Resource-based properties (or their subresources) unique, with a handful of exceptions (properties marked with PROPERTY_USAGE_ALWAYS_DUPLICATE).

I would expect the behavior would be that creating a duplicate of a node would make a new copy that is a unique node unconnected to the original. E.g. let's say I have to make two identical decks in a blackjack game, one for the dealer and one for the player, but I want their positions to be different (or, maybe their sizes, etc). The current behavior I'm seeing is that changing such parameters on the duplicate also changes them on the original.

briankellydev avatar Jul 12 '24 21:07 briankellydev

I would expect the behavior would be that creating a duplicate of a node would make a new copy that is a unique node unconnected to the original.

This is being tracked in https://github.com/godotengine/godot-proposals/issues/317 and https://github.com/godotengine/godot-proposals/issues/4672.

Calinou avatar Jul 12 '24 22:07 Calinou

After thinking for a while why copying resources nested in arrays didn't work, I opened github and searched for this issue as expected. The document should be adjusted quickly to avoid more victims. 🌝

shanayyy avatar Jul 14 '24 09:07 shanayyy

Here's the PR I made to add this exception to the docs: https://github.com/godotengine/godot/pull/94142

AdriaandeJongh avatar Jul 14 '24 14:07 AdriaandeJongh

For anyone wondering how to circumvent this while it gets fixed:

You can create a method in your resource that duplicates itself, then also duplicate each of the elements of the array in a for loop and reutrn the new resource. This should work in the mean time

moshujsg avatar Jul 15 '24 10:07 moshujsg