Struggling to implement Save/Load feature

Godot Version

4.5.1.stable

Question

I have a script named “SaveLoad”. I made it an Autoload, so I can access the save/load mechanic, no matter the scene it comes from. However, when I tried the code below, I get an “Invalid access to properties or key” error on the “contents_to_save[keys] = data.contents_to_save[keys]” line.

extends Node

const save_location = "user://SaveFile.json"

var contents_to_save: Dictionary = {
	"Cities": WorldMap.cities,
	"Active_cities": WorldMap.active_cities,
	"Weekly_expenses": WorldMap.weekly_expenses,
	"Location_purchase": WorldMap.location_purchase,
	"Airship_purchase": WorldMap.airship_purchase
}

# Called when the node enters the scene tree for the first time.
func _save():
	var file = FileAccess.open(save_location, FileAccess.WRITE)
	file.store_var(contents_to_save.duplicate())
	file.close()

func _load():
	if FileAccess.file_exists(save_location):
		var file = FileAccess.open(save_location, FileAccess.READ)
		var data = file.get_var()
		file.close()
		
		for keys in contents_to_save.keys():
			contents_to_save[keys] = data.contents_to_save[keys]
1 Like

you can’t loop over dictionary (tetchily you can but it is undersized )

i think that is the problem

While loading, You should iterate over data.contents_to_save, not contents_to_save.
Also, you can directly iterate over the keys of the dictionary like below, you don’t need to specify .keys()

# you should probably clear the dictionary as well but it depends on your use-case
		contents_to_save.clear() 
		for key in data.contents_to_save:
			contents_to_save[key] = data.contents_to_save[key]
1 Like

Haha. I Initially thought lastbender was right, since it looked like the right answer. But then I had to look at the code again, since “Invalid access to properties or key” means whatever you were accessing doesn’t exist.
So I actually went and tested the code and sure enough, the problem was that you were trying to access data.contents_to_save[keys] instead of data[keys] (since data is the dictionary that you stored).
So the last part of _load() should be:

    for keys in contents_to_save.keys():
        contents_to_save[keys] = data[keys]

Of course, you don’t need to loop through contents_to_save.keys(), as mentioned, you can just loop through contents_to_save.
Also, you don’t need to duplicate() the dictionary when storing it in the file.
And also, it’s just funny that you are storing binary data into a .json file (that doesn’t really affect anything).

1 Like

A couple of comments in addition to @zigg3c’s answer:

  • you can totally loop over a dictionary, though I wouldn’t suggest it in this case
  • you’re giving the file a .json extension, but it’s not going to be JSON, it’s going to be Godot’s TOML-like format
  • given your RPG-looking save data, you might want to give some thought to loading the file raw into a dictionary, and including a save format version entry to the dictionary; otherwise, when you add/change/remove one of the fields, your old save files will confuse your loader…
2 Likes

Couple notes for clarification:

  1. This game isn’t an RPG, it’s a tycoon game. The “contents_to_save” dictionary is just saving values of variables and dictionaries that are used in the game.
  2. What code would you recommend for saving and loading in this case? I currently have the “save” button in the game itself, while the “load” button is in the main menu only. When I tested the game, the load button didn’t load anything, so I doubt the code I learned on Youtube works in general.

Use a custom resource class instead.

Your code is not “loading” anything because you never assign any loaded stuff to variables in WorldMap

Godot’s TOML format will save an entire arbitrarily nested dictionary for you in one swell foop if you like.

Personally, in Stahldrache I have a save state dictionary, and I load/save those in a single line much like you’re doing in your save code above.

I then pull out a save version number, and have code that looks more or less like:

func restore_game_from_data(d: Dictionary) -> void:
    var version: int = d.get("Version", -1)
    match version:
        -1: # Unversioned!
            refuse_to_load()

        1: # Old version
            GlobalData.Cities       = d.get("Cities")
            GlobalData.ActiveCities = d.get("Active Cities")
            GlobalData.Monorail     = make_new_monorail()
            [...]

        2: # Current version
            GlobalData.Cities       = d.get("Cities")
            GlobalData.ActiveCities = d.get("Active Cities")
            GlobalData.Monorail     = d.get("Monorail")
            [...]
1 Like

We don’t know what’s in WorldMap.cities etc. Might be node references in which case the thing would require a bit different approach.

Oh, definitely, but there’s an infinite variety of possibilities there, so I went with a simple example.

WorldMap.cities is a dictionary that contains the names of cities, their population, cargo amount, and other data (below is an example of the dictionary in question:

var cities = {
	"Los Angeles": [Vector2(78, 165), 1000000, 2000000, 1],
	"San Fransisco": [Vector2(73, 161), 1000000, 1000000, 1],
	"San Diego": [Vector2(80, 166), 1000000, 1000000, 0],
	"Chicago": [Vector2(123, 152), 1000000, 1000000, 0]
}

I updated the code even more, below is what I have:

extends Node

const save_location = "user://SaveFile.json"

var contents_to_save: Dictionary = {
	"Cities": WorldMap.cities,
	"Active_cities": WorldMap.active_cities,
	"Weekly_expenses": WorldMap.weekly_expenses,
	"Location_purchase": WorldMap.location_purchase,
	"Airship_purchase": WorldMap.airship_purchase
}

func _save():
	var file = FileAccess.open(save_location, FileAccess.WRITE)
	file.store_var(contents_to_save.duplicate())
	file.close()

func _load():
	if FileAccess.file_exists(save_location):
		var file = FileAccess.open(save_location, FileAccess.READ)
			file.close()
		
		WorldMap.cities = contents_to_save["Cities"]
		WorldMap.active_cities = contents_to_save["Active_cities"]
		WorldMap.weekly_expenses = contents_to_save["Weekly_expenses"]
		WorldMap.location_purchase = contents_to_save["Location_purchase"]
		WorldMap.airship_purchase = contents_to_save["Airship_purchase"]
		get_tree().change_scene_to_file("res://scenes/World_Map.tscn")

I added the “get_tree()” line at the bottom, so the game would automatically switch to the main game. However, the daved data still doesn’t show.

You’re not calling get_var() in your _load() function at all.
There are other problems as well. Print everything.

extends Node

const save_location = "user://SaveFile.json"

var contents_to_save: Dictionary = {
	"Cities": WorldMap.cities,
	"Active_cities": WorldMap.active_cities,
	"Weekly_expenses": WorldMap.weekly_expenses,
	"Location_purchase": WorldMap.location_purchase,
	"Airship_purchase": WorldMap.airship_purchase
}

func _ready():
	print_debug(contents_to_save)

func _save():
	print_debug(contents_to_save)
	print_debug(contents_to_save.duplicate())
	var file = FileAccess.open(save_location, FileAccess.WRITE)
	file.store_var(contents_to_save.duplicate())
	file.close()

func _load():
	if FileAccess.file_exists(save_location):
		var file = FileAccess.open(save_location, FileAccess.READ)
			file.close()

		print_debug(contents_to_save)

		WorldMap.cities = contents_to_save["Cities"]
		WorldMap.active_cities = contents_to_save["Active_cities"]
		WorldMap.weekly_expenses = contents_to_save["Weekly_expenses"]
		WorldMap.location_purchase = contents_to_save["Location_purchase"]
		WorldMap.airship_purchase = contents_to_save["Airship_purchase"]
		get_tree().change_scene_to_file("res://scenes/World_Map.tscn")

Ok, so I added the print statements for debugging. The print_debug under the ready() and _save() are displaying the expected data. So I made those a comment for now, then added the “print(data)” in the _load() method. It IS displaying the data I intend to save. So it seems the issue is assigning the saved data into the appropriate WorldMap variables (which is on AutoLoad). Keep in mind that the “Cities”, “Active_cities”, “Weekly_expenses”, “Location_purchase”, and “Airship_purchase” are separate variables in the WorldMap script.

Below is the code I have so far:

extends Node

const save_location = "user://SaveFile.json"

var contents_to_save: Dictionary = {
	"Cities": WorldMap.cities,
	"Active_cities": WorldMap.active_cities,
	"Weekly_expenses": WorldMap.weekly_expenses,
	"Location_purchase": WorldMap.location_purchase,
	"Airship_purchase": WorldMap.airship_purchase
}

#func _ready():
	#print(contents_to_save)

func _save():
	#print(contents_to_save)
	#print(contents_to_save.duplicate())
	var file = FileAccess.open(save_location, FileAccess.WRITE)
	file.store_var(contents_to_save.duplicate())
	file.close()

func _load():
	if FileAccess.file_exists(save_location):
		var file = FileAccess.open(save_location, FileAccess.READ)
		var data = file.get_var()
		file.close()
		
		print(data)
		
		WorldMap.Cities = data.get("Cities") <----- I would get "Invalid assignment of property or key 'Cities' with value of type 'Dictionary' on a base object of type 'Node2D (world_map.gd)'."
		WorldMap.Active_cities = data.get("Active_cities")
		WorldMap.Weekly_expenses = data.get("Weekly_expenses")
		WorldMap.Location_purchase = data.get("Location_purchase")
		WorldMap.Airship_purchase = data.get("Airship_purchase")
		get_tree().change_scene_to_file("res://scenes/World_Map.tscn")
1 Like

The error you are getting above means WorldMap.Cities is a Node2D and not a Dictionary as you’ve previously stated.

Let’s maybe take a step back and make sure everything works in the simplest configuration:

extends Node

const save_location = "user://save.dat"

var allow_objects: bool = false
var contents_to_save: Dictionary = {
	"cities": Node2D.new(), # Should print as <EncodedObjectAsID#-0123...>
	"active_cities": 2,
	"weekly_expenses": "String",
	"location_purchase": 4,
	"airship_purchase": Vector2.ZERO,
}

func _ready() -> void:
	_save()
	_load()

func _save():
	var file = FileAccess.open(save_location, FileAccess.WRITE)
	file.store_var(contents_to_save, allow_objects)

func _load():
	if FileAccess.file_exists(save_location):
		var file = FileAccess.open(save_location, FileAccess.READ)
		var data = file.get_var(allow_objects)
		file.close()
		for item in data:
			var value = data[item]
			prints(item, value)

Forget about assigning them to WorldMap for now, does this print out as expected, and does it still print out as expected when you replace the values of contents_to_save with the real values you want to save?

If the dictionary values contain objects, you’ll need to pass that into store_var and get_var in order to serialize and deserialize them correctly. Try changing the allowed_objects variable above the dictionary in the example to true. The first entry should now print <Node2D#0123...>

We’ll get nowhere with this if we don’t know exactly what type of things are you trying to save. If node references are involved in your save data the thing is not so simple. Otherwise it is relatively simple. Show the WorldMap declarations, describe what those things are and how are they’re used.

Below are the variables I am trying to save. The values changes depending on what “purchases” the player makes. Regarding cities, each key stores and array that contains the city’s location on a tilemaplayer, population, cargo amount, and # of airships. The values stored in these arrays change over time based on a timer and once they reach certain values, an airship leaves the city to “deliver” the cargo, resulting in a drop of cargo stored at the city.

extends Node2D

var cities = {
	"Los Angeles": [Vector2(78, 165), 1000000, 2000000, 1],
	"San Fransisco": [Vector2(73, 161), 1000000, 1000000, 1],
	"San Diego": [Vector2(80, 166), 1000000, 1000000, 0],
	"Chicago": [Vector2(123, 152), 1000000, 1000000, 0],
	"Houston": [Vector2(113, 169), 1000000, 1000000, 0],
	"San Antonio": [Vector2(108, 170), 1000000, 1000000, 0],
	"Dallas": [Vector2(110, 166), 1000000, 1000000, 0],
	"New York": [Vector2(142, 156), 1000000, 1000000, 0],
	"Seattle": [Vector2(73, 143), 1000000, 500000, 0],
	"Vancouver": [Vector2(71, 139), 1000000, 500000, 0],
	"Anchorage": [Vector2(31, 113), 1000000, 500000, 0],
	"Detroit": [Vector2(131, 151), 1000000, 500000, 0],
	"Washington DC": [Vector2(140, 160), 1000000, 500000, 0],
	"Mexico City": [Vector2(108, 187), 1000000, 500000, 0],
	"Rio de Janeiro": [Vector2(187, 239), 1000000, 500000, 0],
	"London": [Vector2(251, 135), 1000000, 500000, 0],
	"Paris": [Vector2(254, 142), 1000000, 500000, 0],
	"Berlin": [Vector2(270, 134), 1000000, 500000, 0],
	"Rome": [Vector2(270, 153), 1000000, 500000, 0],
	"Seoul": [Vector2(437, 160), 1000000, 500000, 0],
	"Tokyo": [Vector2(456, 163), 1000000, 500000, 0],
	"Beijing": [Vector2(423, 156), 1000000, 500000, 0],
	"Taipei": [Vector2(430, 177), 1000000, 500000, 0]
}

var active_cities = [
	"Los Angeles",
	"San Fransisco",
	"San Diego"
]

var weekly_expenses = {
	"Maintenance_Location": 500,
	"Maintenance_Airship": 250,
	"Electricity": 250,
	"Fuel": 50, #Accounted for only in the create_airship_icon() method
	"Property Tax": 500,
	"Wages and Salaries": 1000,
	"Loan repayment": 0
}

var location_purchase: int = 500000
var airship_purchase: int = 1000000

Since there are no object references there you can just use store_var() and get_var() directly on those variables:

When saving:

# open the file for writing
f.store_var(cities)
f.store_var(active_cities)
f.store_var(weekly_expenses)

When loading (the order must be exactly the same):

# open the file for reading
cities = f.get_var()
active_cities = f.get_var()
weekly_expenses = f.get_var()

That’s all there is to it.

1 Like

There’s been something wrong with something, somewhere in this whole chain of what does who in where.

I’m just going to say that based on your latest post, and second to latest post, the problem is that in the last one you have the variable named cities and in the other one you are trying to assign the value from the save to Cities (note the capital C).

Make sure you’ve typed the name of the variable correctly. If this doesn’t solve it, consider abandoning saves and making it a Roguelike.

1 Like

Wiser words have never been spoken! Can I get your permission to make fridge magnets with this printed on?
:smiley:

1 Like

I was joking, but this might be another github issue related to errors in a day:

var object = Object.new()
object.whatever = "value"

will report Invalid assignment of property or key 'whatever' with value of type 'String' on a base object of type 'Object'.

Which at first glance should be fine, however I got tripped up above. I thought the property exists, but the type was wrong. The error shows when the property doesn’t exist at all, however.

A better error would be something along the lines of Invalid assignment of property 'whatever': property doesn't exist.

Ah well…