How to save nested resources?

Godot Version

Godot 4.2.1 stable

Question

I’m using resources to save game states. So far everything is working fine, but it seems like my nested resources aren’t saved.

Here’s a rough example of what I’m trying to do:
“Fridge” object has a resource: “Content”, which has a resource inside “water bottle”

When I use ResourceLoader, I’m able to save and load the Content resource as expected (it has water bottle in it)
But the data of the water bottle resource (full/half-full/empty) is NOT being saved in this case.

Is there a way to get this to save too? Or do I need to save the nested resources separately?

Thanks in advance!

1 Like

Please build a small version of the issue so that someone can better help you. It would help to run the same code to debug it.

1 Like

Only @exported variables or variables marked with PROPERTY_USAGE_STORAGE in Object._get_property_list() are serialized to disk

I’ve exported all needed variables, to no effect.
I’ll look into the PROPERTY_USAGE_STORAGE to learn more about it.

I’ve created the smallest possible project I could think of to show my issue:
In this project there are 2 custom resources, one is nested within the other. I can load the main resource, but its values or the values of the child resource do not seem to get loaded/saved properly. I assume my thinking is wrong on how this should work and would appreciate any pointers.

I’m poking around with it now. May take me a while until my brain warms up!

1 Like

Well, I confess your example code confused me somewhat. In the end I just hacked my own script to try help you. It uses print, so see the output there.

You need a scene with save and load buttons. Wire them to this script:

extends Control

## Demo of saving a resource with a sub-resource
## Be sure to open dbat_resource.tres in an editor to see how Godot does it.

var fridge:Fridge

## Using ready to fill the fridge
func _ready() -> void:
	fridge = Fridge.new() #<-- this will detect the Fridge class automagically.
	var waterbottle = Thing.new() #<-- same
	var apple = Thing.new()
	waterbottle.name = "expensive water"
	apple.name = "a mouldy apple"
	fridge.contents.append(waterbottle)
	fridge.contents.append(apple)

	print("Fridge is full, hit save, then load.")


func _on_save_pressed() -> void:
	var err = ResourceSaver.save(fridge,
		"res://dbat/dbat_resource.tres",
		ResourceSaver.FLAG_CHANGE_PATH)
	if not err:
		print("saved ok")
	else:
		print(err)


func _on_load_pressed() -> void:
	var tmp = ResourceLoader.load(
		"res://dbat/dbat_resource.tres")
	if tmp:
		fridge = tmp
		print("loaded ok")
		print("  fridge:", fridge)
		for thing in fridge.contents:
			print("contents:", thing.name)
	else:
		print("oops")

Here’s the Fridge:

class_name Fridge
extends Resource

@export var contents:Array[Thing]

and the Things that go in it:

class_name Thing
extends Resource

@export var name:String

I hope that helps you!

1 Like

CACHE_MODE_IGNORE is broken. It’s a known bug `ResourceLoader.CACHE_MODE_IGNORE` does not ignore the resource cache · Issue #59669 · godotengine/godot · GitHub

1 Like

Thanks for this!

To truly test this for my purposes, I added another button that calls the following function to your script:

func _on_create_new_thing_pressed() -> void:
	var coin_flip = randi_range(0,1)
	var random_value = randi_range(1,10)
	
	var new_thing = Thing.new()
	if coin_flip == 0:
		new_thing.name = "waterbottle"
	else:
		new_thing.name = "apple"
		
	new_thing.value = random_value
	
	print("Created new thing: ", new_thing.name, " with value ", new_thing.value)
	
	fridge.contents.clear()
	fridge.contents.append(new_thing)
	print("Fridge emptied and new thing added.")

I’ve also added another value to the Thing class, it now looks like this:

class_name Thing
extends Resource

@export var name:String
@export var value:int

When I now run this and save a randomly generated thing in the fridge, I can load the correct thing, but the things value is incorrect / not saved.
This is the main issue I run into.

Seems ok to me. Here’s the code in full. (Thing and Fridge you already have.)

extends Control

## Demo of saving a resource with a sub-resource
## Be sure to open dbat_resource.tres in an editor to see how Godot does it.

var fridge:Fridge

## Using ready to fill the fridge
func _ready() -> void:
	fridge = Fridge.new()

func _on_create_new_thing_pressed() -> void:
	var coin_flip = randi_range(0,1)
	var random_value = randi_range(1,10)
	var new_thing = Thing.new()
	if coin_flip == 0:
		new_thing.name = "waterbottle"
	else:
		new_thing.name = "apple"
	new_thing.value = random_value
	print("Created new thing: ", new_thing.name, " with value ", new_thing.value)
	fridge.contents.clear()
	fridge.contents.append(new_thing)
	print("Fridge emptied and new thing added.")


func _on_save_pressed() -> void:
	var err = ResourceSaver.save(fridge,
		"res://dbat/dbat_resource.tres",
		ResourceSaver.FLAG_CHANGE_PATH)
	if not err:
		print("saved ok")
	else:
		print(err)


func _on_load_pressed() -> void:
	var tmp = ResourceLoader.load(
		"res://dbat/dbat_resource.tres")
	if tmp:
		fridge = tmp
		print("loaded ok")
		print("  fridge:", fridge)
		for thing in fridge.contents:
			print("contents:", thing.name, " value:", thing.value)
	else:
		print("oops")

I run it and press Add then Save and then Load. Output:

Created new thing: apple with value 10
Fridge emptied and new thing added.
saved ok
loaded ok
  fridge:<Resource#-9223372001354185480>
contents:apple value:10
Created new thing: waterbottle with value 8
Fridge emptied and new thing added.
saved ok
loaded ok
  fridge:<Resource#-9223372001354185480>
contents:waterbottle value:8

What does your output say?

BTW am using 4.2.1 stable.

1 Like

First of all, thanks for still looking into this with me, appreciate the help!

In your run, it seems like you create a new thing and then immediately save it, so the saved fridge and the temp fridge contents are always the same.

When I save contents, then create a new thing and then load the fridge without saving it, i don’t get the expected results:

Fridge is full, hit save, then load.
saved ok
loaded ok
  fridge:<Resource#-9223372003954653958>
contents:expensive water with value 5
contents:a mouldy apple with value 3
Created new thing: waterbottle with value 10
Fridge emptied and new thing added.
loaded ok
  fridge:<Resource#-9223372003954653958>
contents:waterbottle with value 10

As you see in the printout above, after loading, the fridge should have the expensive water and apple, not the water bottle. At least that’s what I’m trying to achieve.

Ok, new attempt :smiley:

I think the fix is ResourceLoader.CACHE_MODE_IGNORE in ResourceLoader.load

extends Control

## Demo of saving a resource with a sub-resource
## Be sure to open dbat_resource.tres in an editor to see how Godot does it.

var fridge:Fridge

func _on_create_new_thing_pressed() -> void:
	if not fridge:
		fridge = Fridge.new()

	var coin_flip = randi_range(0,1)
	var random_value = randi_range(1,10)
	var new_thing = Thing.new()
	if coin_flip == 0:
		new_thing.name = "waterbottle"
	else:
		new_thing.name = "apple"
	new_thing.value = random_value
	print("\nCreated new thing: ", new_thing.name, " with value ", new_thing.value)
	fridge.contents.clear()
	fridge.contents.append(new_thing)
	print("Fridge restocked.")


func _on_save_pressed() -> void:
	if not fridge:
		print("Add or Load first.")
		return
	var err = ResourceSaver.save(fridge,
		"res://dbat/dbat_resource.tres",
		ResourceSaver.FLAG_CHANGE_PATH)
	if not err:
		print("saved ok")
	else:
		print(err)


func _on_load_pressed() -> void:
	var tmp:Fridge = ResourceLoader.load(
		"res://dbat/dbat_resource.tres", "",
		ResourceLoader.CACHE_MODE_IGNORE #<-- seems to make the difference!
	)
	if tmp:
		fridge = tmp
		print("Loaded ok")
		print("  Fridge:", fridge)
		for thing in fridge.contents:
			print("  Contents:", thing.name, " value:", thing.value)
	else:
		print("Oops")

Output edited:

(Started with Add)
Created new thing: waterbottle with value 1
Fridge restocked.

(Attempted a Load here - I had deleted the tres file)
ERROR: blah blah
Oops

(Then I Saved)
saved ok

(Now a Load)
Loaded ok
  Fridge:<Resource#-9223371994827848500>
  Contents:waterbottle value:1

(Now Add to restock)
Created new thing: apple with value 5
Fridge restocked.

(Save)
saved ok

(Load)
Loaded ok
  Fridge:<Resource#-9223371984912513794>
  Contents:apple value:5
1 Like

Thanks for this!
This works on my end as well, just as expected!

The bad news: In my main project that I’m trying to get this to work, it still does not seem to function that way.
As @mrcdk mentioned, CACHE_MODE_IGNORE still seems to have some issues, so that might be the cause, hoping it will be resolved in 4.3

In the meantime, I save the specific values I need from my nested resources separately and overwrite them after I’ve loaded them as a workaround. Not ideal, but does the job.

Thanks again everyone!

1 Like

Yeah, Godot 4 has been a truck and I’ve been hanging onto a rope off the back, being dragged over all kinds of nasty stuff for months now. Each time I have a thing working, something else breaks. :smiley:

Courage!

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.