Would like to understand: Custom Resource: null when preloading

Godot Version

v4.5.1 for Linux x86_64

Question

Was torn between posting this in help and the general resource area. I don’t have an issue so much as I would like to understand what’s up with this behavior.

I have a custom resource called InventoryItem.gd. It appears like so:

class_name InventoryItem
extends Resource

@export var display_name: String
@export var icon: Texture2D
@export_multiline var description: String

@export_file_path var drop_instance_path
@export var equip_instance: PackedScene
@export var use_instance: PackedScene

I have a few inherited scenes like torch.tres and coins.tres that define these properties. When I try to instance them in a scene like so:

var item: InventoryItem = preload("res://items/inventory/foo.tres")
print(item == null)
>>> true

Preload does appear to produce a valid, empty, generic resource, but casting it to InventoryItem causes it to go null, as happens when doing the implied type:

var item = preload("res://items/inventory/foo.tres")

I’m surprised to see there’s no resource path there.

On the flip side, using load instead of preload appears to populate things correctly and can generate an InventoryItem.

For comedy value, the following also appears to work, too, but seems like a bad idea:

var item = InventoryItem.new()
item.resource_path = "res://items/inventory/doorkey.tres"

I found this potentially related issue: Error accessing custom resource caused by order of preload · Issue #87019 · godotengine/godot · GitHub but it’s not raising a get_index problem so I think that’s perhaps tangential.

Is it just the case that custom resources always need to be loaded with ‘load’ instead of preload? I didn’t see that in the docs.

preload() should work. You may be doing something else wrong. Perhaps provide a minimal reproducible example.

Hmm. :thinking: Not so easy to reproduce in a new project. Perhaps it’s because of how my inventory system works. InventoryItem (a resource) will reference a packed scene. The packed scene is something that can be equipped, so it’s a Node instead of a resource. The packed scene is instanced, but when it’s unequipped it creates a new InventoryItem to get added back to the player inventory. Perhaps this circular set of actions is the root of the issue.

An example/diagram for clarity, at the risk of redundancy:

AxeItem (instance of InventoryItem) has a PackedScene called AxeWeapon.

AxeWeapon (instance of Node3D) calls player.add_item(preload("res://items/inventory/axe_item.tres"))) when axe_weapon.unequip() is called.

Perhaps that’s it? My speculation is that it’s an interaction between a packedscene and a resource, but that’s only a guess.

Yeah, sounds like a circular dependency. You should avoid those. Best to redesign your system so it doesn’t contain any.

Managed to reproduce it. I think it is a circular dependency issue. Unfortunate, given that the ‘load’ workaround is functional. Now I have to choose between fixing the hack and moving forward. :’)

EDIT: Can’t attach the minimal zip repro, but I can add all the files.

globals.gd:

extends Node

var main_game_ref: Main

main.gd:

class_name Main extends Control

func _ready() -> void:
	Globals.main_game_ref = self
	var axe_item: InventoryItem = preload("res://axe_item.tres")
	var axe_weapon = axe_item.equip_scene.instantiate()
	axe_weapon.use()

func add_to_inventory(new_item: InventoryItem):
	print("Added to inventory: %s" % [new_item.message,])

inventory_item.gd:

class_name InventoryItem extends Resource

@export var message: String
@export var equip_scene: PackedScene

axe_item.tscn: (Inherits inventory_item)

[gd_resource type="Resource" script_class="InventoryItem" load_steps=3 format=3 uid="uid://gq4bmftkor7b"]

[ext_resource type="Script" uid="uid://cxbiee8hce5a2" path="res://inventory_item.gd" id="1_78mgn"]
[ext_resource type="PackedScene" uid="uid://bjvmaducfdlw8" path="res://axe_weapon.tscn" id="1_mpcti"]

[resource]
script = ExtResource("1_78mgn")
message = "Pretend this is an axe."
equip_scene = ExtResource("1_mpcti")
metadata/_custom_type_script = "uid://cxbiee8hce5a2"

axe_weapon.gd:

extends Control

func use():
	print("Axe used.  Unequipping and adding back to inventory.")
	self.unequip()

func unequip():
	var inv_item: InventoryItem = preload("res://axe_item.tres")
	Globals.main_game_ref.add_to_inventory(inv_item)

axe_weapon.tscn:

[gd_scene load_steps=2 format=3 uid="uid://bjvmaducfdlw8"]

[ext_resource type="Script" uid="uid://tg3vqjlxjkc2" path="res://axe_weapon.gd" id="1_onghr"]

[node name="AxeWeapon" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_onghr")