Custom Resources saving not working in 4.6.3 anymore

Godot Version

4.6.3

Question

Hi, i did update from 4.4 to 4.6.3. Now my plugin to edit and save resources not working anymore. When i edit and save a .tres i can see the changes in the inspector. When going to the file system and open the .tres with notepad, there are no changes. ResourceSaver.save gives me no errors. For sure, when running the game the loaded .tres is also without changes. Any idea?

func on_save_button_pressed():

    var npc_prop = npc_dropdown.get_item_text(npc_dropdown.selected)
    var error = ResourceSaver.save(current_npc_prop, "res://resources/npc_properties/" + npc_prop + ".tres")
    if error != OK:
        printerr("Failure!")
    else:
        print("NPC property: " + str(npc_prop) + ".tres saved!!!")
    load_npc_properties()

already tried (with no luck):


current_npc_prop.take_over_path("res://resources/npc_properties/" + npc_prop + ".tres")
current_npc_prop = current_npc_prop.duplicate(true)

This might be a long shot but do you use @export in the variables that you want to be saved? That’s how resources are serialized and saved in the system. I had the same issue where the variables would just reset, and adding @export & @export_storage worked for me.

Also I see that you’re saving it to the “res://”, but you should use “user://” for saving data and loading it in runtime. Hope these two will fix your issue.

For sure i used @export.
Saving is done via EditorPlugin. Only loading in game, for now. I am aware to change or create data for saving in user:// later.

In 4.4 everything works fine.

user:// isn’t the issue here, this is an editor plugin writing project data under res://. @export you already have.

ResourceSaver returning OK while Notepad still shows the old file usually means you’re not writing the object you think (or nested resources are still shared ExtResources and never get their own write). Same pattern showed up after a 4.6 upgrade here:

Right before save, print the property you just changed on current_npc_prop. If that print is already old, the inspector isn’t editing that instance.
Nested resources – if current_npc_prop holds other Resources, make them unique before save, then take_over_path + save like:

var path := "res://resources/npc_properties/%s.tres" % npc_prop
var to_save := current_npc_prop.duplicate(true)
to_save.take_over_path(path)
var err := ResourceSaver.save(to_save, path)
EditorInterface.get_resource_filesystem().update_file(path)

Difference from your version here is: duplicate into a fresh to_save after edits, path+save that instance, then update_file.

Right after save, read the file with FileAccess (or check modified time). If that is still old, the write isn’t hitting that path.
Also, how do you assign current_npc_prop when the dropdown changes; load(), cache, duplicate?

Thanks japonbaligi, but not working.

I did print the property i just changed before save and it shows the change.
No nested resources.
As you see in the script, i did reload everything after save. (ResourceLoader.CACHE_MODE_IGNORE)
It´s a bit wired, after save, i see a new timestamp in the filesystem.
After reload with the plugin, the changes are still there but as told, when i look into the file with notepade, nothing.
So i guess there is something not working with saving and the cache mode aswell.

Here some of the code:

func load_npc_properties():
	var index = 0
	all_properties.clear() ## dict holding all the properties
	var available_prop = list_files_in_directory("res://resources/npc_properties/")
	for prop in available_prop:
		if prop.ends_with(".tres"):
			var prop_res = ResourceLoader.load("res://resources/npc_properties/" + str(prop),"",ResourceLoader.CACHE_MODE_IGNORE)
			all_properties[index] = prop_res ## dcit with index, so i can use the index from the dropdown selection
			index += 1
func on_npc_selected(index):
	current_npc_prop_index = index
	current_npc_prop = all_properties[index]
...

I found something, but no solution.
When i create a new resource, from my npc_properties.gd.

extends Resource

class_name NPCProperties
## for npcs

signal npc_died

@export var name: String = ""
@export var health: int
@export var max_health: int
@export var attack_strenght: int
@export var defense_strenght: int
@export var attack_speed: float
@export var angry: bool

@export var shedule: Dictionary[int,StringName]
#@export var appereance: Dictionary ##
@export var current_activity: String
@export var current_map: String
@export var activity_start_time: float
@export var current_position: Vector2
@export var current_path: Array
@export var managed_by_npc_manager: bool
@export var controlled_by_limbo: bool = false
@export var shedule_pause: bool = false

@export var guard_position: Vector2
@export var movement_queue: Dictionary
@export var movement_queue_order: Dictionary
@export var current_queue_id: int
@export var current_path_segment: int
@export var progress_segment: float
@export var dnd: bool ## is set by npc manager, while sleeping it is true f.e.


func _init() -> void:
	_set_name()

##Call to set NPC name instead of doing by hand.
func _set_name() -> void:
	var path_len = len("res://resources/npc_properties/")
	var name_len = len(resource_path) - (path_len + 5)
	name = resource_path.substr(path_len, name_len)
	#print("NP PROP LOADED. \nRes path: ", resource_path," \nName: ", name)

it looks like this in the .tres:

[gd_resource type="Resource" script_class="NPCProperties" format=3 uid="uid://d20r4gtsd5ipx"]

[ext_resource type="Script" uid="uid://bgnyysqx4wtd7" path="res://resources/npc_properties/npc_properties.gd" id="1_boxro"]

[resource]
script = ExtResource("1_boxro")
metadata/_custom_type_script = "uid://bgnyysqx4wtd7"

If i add a property, like the dict for the movement_queue within the inspector, save the file, then adding another entry into the dict via the plugin works.
So Godot needs to have the variables set before.

But as long there is no empty variable set before, the plugin cannot write to it.

With Godot 4.4 a newly created resource looks like this:

[gd_resource type="Resource" script_class="NPCProperties" load_steps=2 format=3 uid="uid://d20r4gtsd5ipx"]

[ext_resource type="Script" uid="uid://bgnyysqx4wtd7" path="res://resources/npc_properties/npc_properties.gd" id="1_boxro"]

[resource]
script = ExtResource("1_boxro")
name = ""
health = 0
max_health = 0
attack_strenght = 0
defense_strenght = 0
attack_speed = 0.0
angry = false
shedule = Dictionary[int, StringName]({})
current_activity = ""
current_map = ""
activity_start_time = 0.0
current_position = Vector2(0, 0)
current_path = []
managed_by_npc_manager = false
controlled_by_limbo = false
shedule_pause = false
guard_position = Vector2(0, 0)
movement_queue = {}
movement_queue_order = {}
current_queue_id = 0
current_path_segment = 0
progress_segment = 0.0
dnd = false
metadata/_custom_type_script = "uid://bgnyysqx4wtd7"

So the question is, why in v.4.6.3 a newly .tres is just empty, without my variables?

That empty .tres is expected in 4.6, not your plugin failing to “create” properties.

Godot skips writing @export values that still match the script defaults. 4.4 dumped every health = 0 / name = ""; 4.6 leaves those out. So a new file often only has the script reference and metadata, no property lines. Same idea as here

Why plugin looks broken: if the value you set is still “default-ish” (empty Dictionary, 0, “”, …), ResourceSaver writes nothing for that property. Inspector “touch + save” once puts a real entry in the file, then later edits stick; matches what you saw with movement_queue.

Also drop property writes from _init (_set_name). _init runs before file values are applied, and it fights this omit-defaults behavior: related doc

Set name when you create/save the asset instead.
For the plugin, before ResourceSaver.save:

# make sure you're not mutating a "still default" dict in place only
current_npc_prop.movement_queue = current_npc_prop.movement_queue.duplicate()
# set anything that must persist to a non-default, or assign the whole property
current_npc_prop.emit_changed()
ResourceSaver.save(current_npc_prop, path)

Check the .tres for a property you set to something obviously non-default (e.g. health = 7). If that line appears, saving works; the file is just sparse on purpose.

Thanks, that is interesting to know…guess i need to read the changelog more precisely.
Anyway, i tried with the duplicate of the dict, but no luck.

I put the set name in the plugin, and there it is working. I also set just an array for testing.
This is also working.

Only the dict not, for some reason.

The tres. after setting name and array:

[gd_resource type="Resource" script_class="NPCProperties" format=3 uid="uid://djimh2sso8waj"]

[ext_resource type="Script" uid="uid://bgnyysqx4wtd7" path="res://resources/npc_properties/npc_properties.gd" id="1_7mpsw"]

[resource]
script = ExtResource("1_7mpsw")
name = "alf"
current_path = [0.0]
metadata/_custom_type_script = "uid://bgnyysqx4wtd7"

The godot output before saving, print(current_npc_prop.movement_queue)

{ "sleeping": { "westergaard_home": [{ "from_alf_sleeping": [(64.0, -55.0)] }, { "to_alf_sleeping": [(64.0, -55.0)] }] }, "idle": { "westergaard_home": [{ "from_alf_idle": [(2.0, -40.0)] }, { "to_alf_idle": [(2.0, -40.0)] }] } }
NPC property: alf.tres saved!!!

After searching “all” the reported issues with dicts and resources on github, i finally found a solution.

To .clear() the dict before assigning the duplicate has solved my issue.
current_npc_prop.movement_queue.clear()

func on_save_button_pressed():
	print(current_npc_prop.movement_queue)
		
	var dict_backup = current_npc_prop.movement_queue.duplicate()
	current_npc_prop.movement_queue.clear()  
	current_npc_prop.movement_queue = dict_backup  # Wiederherstellen
	
	var npc_prop_name = npc_dropdown.get_item_text(npc_dropdown.selected)
	var path = "res://resources/npc_properties/" + npc_prop_name + ".tres"
	
	current_npc_prop.name = npc_prop_name
	#current_npc_prop.emit_changed()
	#var to_save = current_npc_prop#.duplicate(true)
	#to_save.take_over_path(path)

	var error = ResourceSaver.save(current_npc_prop, path)
	EditorInterface.get_resource_filesystem().update_file(path)
	if error != OK:
		printerr("Failure!")
	else:
		print("NPC property: " + str(npc_prop_name) + ".tres saved!!!")
	load_npc_properties()